1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-19 22:49:08 -05:00

Add excusing for absence (#533)

This commit is contained in:
Kacper Ziubryniewicz 2020-01-25 18:07:25 +01:00 committed by Rafał Borcz
parent 0c5d45717c
commit 19f495cba6
34 changed files with 2132 additions and 47 deletions

File diff suppressed because it is too large Load Diff

View File

@ -35,9 +35,9 @@ class AttendanceLocalTest {
@Test
fun saveAndReadTest() {
attendanceLocal.saveAttendance(listOf(
Attendance(1, 2, LocalDate.of(2018, 9, 10), 0, "", "", false, false, false, false, false, false),
Attendance(1, 2, LocalDate.of(2018, 9, 14), 0, "", "", false, false, false, false, false, false),
Attendance(1, 2, LocalDate.of(2018, 9, 17), 0, "", "", false, false, false, false, false, false)
Attendance(1, 2, 3, LocalDate.of(2018, 9, 10), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name),
Attendance(1, 2, 3, LocalDate.of(2018, 9, 14), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.WAITING.name),
Attendance(1, 2, 3, LocalDate.of(2018, 9, 17), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name)
))
val attendance = attendanceLocal

View File

@ -2,8 +2,8 @@ package io.github.wulkanowy.data.repositories.timetable
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalDateTime.now
import io.github.wulkanowy.sdk.pojo.Timetable as TimetableRemote
import io.github.wulkanowy.data.db.entities.Timetable as TimetableLocal
import io.github.wulkanowy.sdk.pojo.Timetable as TimetableRemote
fun createTimetableLocal(start: LocalDateTime, number: Int, room: String = "", subject: String = "", teacher: String = "", changes: Boolean = false): TimetableLocal {
return TimetableLocal(

View File

@ -61,6 +61,7 @@ import io.github.wulkanowy.data.db.migrations.Migration18
import io.github.wulkanowy.data.db.migrations.Migration19
import io.github.wulkanowy.data.db.migrations.Migration2
import io.github.wulkanowy.data.db.migrations.Migration20
import io.github.wulkanowy.data.db.migrations.Migration21
import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5
@ -102,7 +103,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 20
const val VERSION_SCHEMA = 21
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf(
@ -124,7 +125,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration17(),
Migration18(),
Migration19(sharedPrefProvider),
Migration20()
Migration20(),
Migration21()
)
}

View File

@ -15,6 +15,9 @@ data class Attendance(
@ColumnInfo(name = "diary_id")
val diaryId: Int,
@ColumnInfo(name = "time_id")
val timeId: Int,
val date: LocalDate,
val number: Int,
@ -33,7 +36,13 @@ data class Attendance(
val excused: Boolean,
val deleted: Boolean
val deleted: Boolean,
val excusable: Boolean,
@ColumnInfo(name = "excuse_status")
val excuseStatus: String?
) : Serializable {
@PrimaryKey(autoGenerate = true)

View File

@ -0,0 +1,13 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration21 : Migration(20, 21) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Attendance ADD COLUMN excusable INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE Attendance ADD COLUMN time_id INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE Attendance ADD COLUMN excuse_status TEXT DEFAULT NULL")
}
}

View File

@ -3,8 +3,11 @@ package io.github.wulkanowy.data.repositories.attendance
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Absent
import io.reactivex.Single
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalTime
import javax.inject.Inject
import javax.inject.Singleton
@ -19,6 +22,7 @@ class AttendanceRemote @Inject constructor(private val sdk: Sdk) {
studentId = semester.studentId,
diaryId = semester.diaryId,
date = it.date,
timeId = it.timeId,
number = it.number,
subject = it.subject,
name = it.name,
@ -27,9 +31,20 @@ class AttendanceRemote @Inject constructor(private val sdk: Sdk) {
exemption = it.exemption,
lateness = it.lateness,
excused = it.excused,
deleted = it.deleted
deleted = it.deleted,
excusable = it.excusable,
excuseStatus = it.excuseStatus?.name
)
}
}
}
fun excuseAbsence(semester: Semester, absenceList: List<Attendance>, reason: String?): Single<Boolean> {
return sdk.switchDiary(semester.diaryId, semester.schoolYear).excuseForAbsence(absenceList.map { attendance ->
Absent(
date = LocalDateTime.of(attendance.date, LocalTime.of(0, 0)),
timeId = attendance.timeId
)
}, reason)
}
}

View File

@ -41,4 +41,8 @@ class AttendanceRepository @Inject constructor(
}).map { list -> list.filter { it.date in startDate..endDate } }
}
}
fun excuseForAbsence(semester: Semester, attendanceList: List<Attendance>, reason: String? = null): Single<Boolean> {
return remote.excuseAbsence(semester, attendanceList, reason)
}
}

View File

@ -0,0 +1,7 @@
package io.github.wulkanowy.data.repositories.attendance
enum class SentExcuseStatus(val id: Int = 0) {
WAITING,
ACCEPTED,
DENIED
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.ui.modules.attendance
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import io.github.wulkanowy.data.db.entities.Attendance
class AttendanceAdapter<T : IFlexible<*>> : FlexibleAdapter<T>(null, null, true) {
var excuseActionMode: Boolean = false
var onExcuseCheckboxSelect: (attendanceItem: Attendance, checked: Boolean) -> Unit = { _, _ -> }
}

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.attendance
import android.content.DialogInterface.BUTTON_POSITIVE
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
@ -10,8 +11,9 @@ import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -24,6 +26,7 @@ import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.dialog_excuse.*
import kotlinx.android.synthetic.main.fragment_attendance.*
import org.threeten.bp.LocalDate
import javax.inject.Inject
@ -35,7 +38,13 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
lateinit var presenter: AttendancePresenter
@Inject
lateinit var attendanceAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
lateinit var attendanceAdapter: AttendanceAdapter<AbstractFlexibleItem<*>>
override val excuseSuccessString: String
get() = getString(R.string.attendance_excuse_success)
override val excuseNoSelectionString: String
get() = getString(R.string.attendance_excuse_no_selection)
companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE"
@ -49,6 +58,34 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize
override val excuseActionMode: Boolean get() = attendanceAdapter.excuseActionMode
private var actionMode: ActionMode? = null
private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val inflater = mode.menuInflater
inflater.inflate(R.menu.context_menu_excuse, menu)
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.title = getString(R.string.attendance_excuse_title)
return presenter.onPrepareActionMode()
}
override fun onDestroyActionMode(mode: ActionMode) {
presenter.onDestroyActionMode()
actionMode = null
}
override fun onActionItemClicked(mode: ActionMode, menu: MenuItem): Boolean {
return when (menu.itemId) {
R.id.excuseMenuSubmit -> presenter.onExcuseSubmitButtonClick()
else -> false
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -66,6 +103,7 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
override fun initView() {
attendanceAdapter.setOnItemClickListener(presenter::onAttendanceItemSelected)
attendanceAdapter.onExcuseCheckboxSelect = presenter::onExcuseCheckboxSelect
with(attendanceRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context)
@ -83,6 +121,8 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
attendanceNavDate.setOnClickListener { presenter.onPickDate() }
attendanceNextButton.setOnClickListener { presenter.onNextDay() }
attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() }
attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f))
}
@ -115,6 +155,10 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
if (::presenter.isInitialized) presenter.onViewReselected()
}
override fun onFragmentChanged() {
if (::presenter.isInitialized) presenter.onMainViewChanged()
}
override fun popView() {
(activity as? MainActivity)?.popView()
}
@ -155,6 +199,10 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showExcuseButton(show: Boolean) {
attendanceExcuseButton.visibility = if (show) VISIBLE else GONE
}
override fun showAttendanceDialog(lesson: Attendance) {
(activity as? MainActivity)?.showDialogFragment(AttendanceDialog.newInstance(lesson))
}
@ -174,10 +222,38 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
}
}
override fun showExcuseDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.attendance_excuse_title)
.setView(R.layout.dialog_excuse)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.create()
.apply {
setButton(BUTTON_POSITIVE, getString(R.string.attendance_excuse_dialog_submit)) { _, _ ->
presenter.onExcuseDialogSubmit(excuseReason.text?.toString().orEmpty())
}
}.show()
}
override fun openSummaryView() {
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance())
}
override fun startActionMode() {
actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback)
}
override fun showExcuseCheckboxes(show: Boolean) {
attendanceAdapter.apply {
excuseActionMode = show
notifyDataSetChanged()
}
}
override fun finishActionMode() {
actionMode?.finish()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())

View File

@ -1,18 +1,22 @@
package io.github.wulkanowy.ui.modules.attendance
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.core.view.isVisible
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.repositories.attendance.SentExcuseStatus
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_attendance.*
class AttendanceItem(val attendance: Attendance) : AbstractFlexibleItem<AttendanceItem.ViewHolder>() {
class AttendanceItem(val attendance: Attendance) :
AbstractFlexibleItem<AttendanceItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_attendance
@ -26,6 +30,34 @@ class AttendanceItem(val attendance: Attendance) : AbstractFlexibleItem<Attendan
attendanceItemSubject.text = attendance.subject
attendanceItemDescription.text = attendance.name
attendanceItemAlert.visibility = attendance.run { if (absence && !excused) VISIBLE else INVISIBLE }
attendanceItemNumber.visibility = GONE
attendanceItemExcuseInfo.visibility = GONE
attendanceItemExcuseCheckbox.visibility = GONE
attendanceItemExcuseCheckbox.isChecked = false
attendanceItemExcuseCheckbox.setOnCheckedChangeListener { _, checked ->
(adapter as AttendanceAdapter).onExcuseCheckboxSelect(attendance, checked)
}
when (if (attendance.excuseStatus != null) SentExcuseStatus.valueOf(attendance.excuseStatus) else null) {
SentExcuseStatus.WAITING -> {
attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_waiting)
attendanceItemExcuseInfo.visibility = VISIBLE
attendanceItemAlert.visibility = INVISIBLE
}
SentExcuseStatus.DENIED -> {
attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_denied)
attendanceItemExcuseInfo.visibility = VISIBLE
}
else -> {
if (attendance.excusable && (adapter as AttendanceAdapter).excuseActionMode) {
attendanceItemNumber.visibility = GONE
attendanceItemExcuseCheckbox.visibility = VISIBLE
} else {
attendanceItemNumber.visibility = VISIBLE
attendanceItemExcuseCheckbox.visibility = GONE
}
}
}
}
}
@ -46,8 +78,20 @@ class AttendanceItem(val attendance: Attendance) : AbstractFlexibleItem<Attendan
return result
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
class ViewHolder(view: View, val adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View
get() = contentView
override fun onClick(view: View?) {
super.onClick(view)
attendanceItemExcuseCheckbox.apply {
if ((adapter as AttendanceAdapter).excuseActionMode && isVisible) {
isChecked = !isChecked
}
}
}
}
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.ui.modules.attendance
import dagger.Module
import dagger.Provides
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@Module
class AttendanceModule {
@Provides
fun provideAttendanceFlexibleAdapter() = AttendanceAdapter<AbstractFlexibleItem<*>>()
}

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.attendance
import android.annotation.SuppressLint
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.repositories.attendance.AttendanceRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
@ -40,6 +41,8 @@ class AttendancePresenter @Inject constructor(
private lateinit var lastError: Throwable
private val attendanceToExcuseList = mutableListOf<Attendance>()
fun onAttachView(view: AttendanceView, date: Long?) {
super.onAttachView(view)
view.initView()
@ -51,11 +54,15 @@ class AttendancePresenter @Inject constructor(
}
fun onPreviousDay() {
view?.finishActionMode()
attendanceToExcuseList.clear()
loadData(currentDate.previousSchoolDay)
reloadView()
}
fun onNextDay() {
view?.finishActionMode()
attendanceToExcuseList.clear()
loadData(currentDate.nextSchoolDay)
reloadView()
}
@ -100,10 +107,59 @@ class AttendancePresenter @Inject constructor(
}
}
fun onMainViewChanged() {
view?.finishActionMode()
}
fun onAttendanceItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is AttendanceItem) {
Timber.i("Select attendance item ${item.attendance.id}")
view?.showAttendanceDialog(item.attendance)
view?.apply {
if (item is AttendanceItem && !excuseActionMode) {
Timber.i("Select attendance item ${item.attendance.id}")
showAttendanceDialog(item.attendance)
}
}
}
fun onExcuseButtonClick() {
view?.startActionMode()
}
fun onExcuseCheckboxSelect(attendanceItem: Attendance, checked: Boolean) {
if (checked) attendanceToExcuseList.add(attendanceItem)
else attendanceToExcuseList.remove(attendanceItem)
}
fun onExcuseSubmitButtonClick(): Boolean {
view?.apply {
return if (attendanceToExcuseList.isNotEmpty()) {
showExcuseDialog()
true
} else {
showMessage(excuseNoSelectionString)
false
}
}
return false
}
fun onExcuseDialogSubmit(reason: String) {
view?.finishActionMode()
excuseAbsence(if (reason != "") reason else null, attendanceToExcuseList.toList())
}
fun onPrepareActionMode(): Boolean {
view?.apply {
showExcuseCheckboxes(true)
showExcuseButton(false)
}
attendanceToExcuseList.clear()
return true
}
fun onDestroyActionMode() {
view?.apply {
showExcuseCheckboxes(false)
showExcuseButton(true)
}
}
@ -157,6 +213,7 @@ class AttendancePresenter @Inject constructor(
showEmpty(it.isEmpty())
showErrorView(false)
showContent(it.isNotEmpty())
showExcuseButton(it.any { item -> item.attendance.excusable })
}
analytics.logEvent("load_attendance", "items" to it.size, "force_refresh" to forceRefresh)
}) {
@ -167,6 +224,39 @@ class AttendancePresenter @Inject constructor(
}
}
private fun excuseAbsence(reason: String?, toExcuseList: List<Attendance>) {
Timber.i("Excusing absence started")
disposable.apply {
add(studentRepository.getCurrentStudent()
.delay(200, MILLISECONDS)
.flatMap { semesterRepository.getCurrentSemester(it) }
.flatMap { attendanceRepository.excuseForAbsence(it, toExcuseList, reason) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {
view?.apply {
showProgress(true)
showContent(false)
showExcuseButton(false)
}
}
.subscribe({
Timber.i("Excusing for absence result: Success")
analytics.logEvent("excuse_absence", "items" to attendanceToExcuseList.size)
attendanceToExcuseList.clear()
view?.apply {
showExcuseButton(false)
showMessage(excuseSuccessString)
}
loadData(currentDate, true)
}) {
Timber.i("Excusing for absence result: An exception occurred")
view?.showProgress(false)
errorHandler.dispatch(it)
})
}
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {

View File

@ -10,6 +10,12 @@ interface AttendanceView : BaseView {
val currentStackSize: Int?
val excuseSuccessString: String
val excuseNoSelectionString: String
val excuseActionMode: Boolean
fun initView()
fun updateData(data: List<AttendanceItem>)
@ -38,11 +44,21 @@ interface AttendanceView : BaseView {
fun showNextButton(show: Boolean)
fun showExcuseButton(show: Boolean)
fun showAttendanceDialog(lesson: Attendance)
fun showDatePickerDialog(currentDate: LocalDate)
fun showExcuseDialog()
fun openSummaryView()
fun startActionMode()
fun showExcuseCheckboxes(show: Boolean)
fun finishActionMode()
fun popView()
}

View File

@ -36,6 +36,7 @@ import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.safelyPopFragments
import io.github.wulkanowy.utils.setOnViewChangeListener
import kotlinx.android.synthetic.main.activity_main.*
import timber.log.Timber
import javax.inject.Inject
class MainActivity : BaseActivity<MainPresenter>(), MainView {
@ -167,6 +168,11 @@ class MainActivity : BaseActivity<MainPresenter>(), MainView {
(navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentReselected()
}
override fun notifyMenuViewChanged() {
Timber.d("Menu view changed")
(navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentChanged()
}
fun showDialogFragment(dialog: DialogFragment) {
navController.showDialogFragment(dialog)
}

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.ui.modules.about.license.LicenseFragment
import io.github.wulkanowy.ui.modules.about.license.LicenseModule
import io.github.wulkanowy.ui.modules.account.AccountDialog
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.attendance.AttendanceModule
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
@ -51,7 +52,7 @@ abstract class MainModule {
}
@PerFragment
@ContributesAndroidInjector
@ContributesAndroidInjector(modules = [AttendanceModule::class])
abstract fun bindAttendanceFragment(): AttendanceFragment
@PerFragment

View File

@ -75,6 +75,7 @@ class MainPresenter @Inject constructor(
notifyMenuViewReselected()
false
} else {
notifyMenuViewChanged()
switchMenuView(index)
true
}

View File

@ -26,6 +26,8 @@ interface MainView : BaseView {
fun notifyMenuViewReselected()
fun notifyMenuViewChanged()
fun setViewTitle(title: String)
fun popView(depth: Int = 1)
@ -33,6 +35,8 @@ interface MainView : BaseView {
interface MainChildView {
fun onFragmentReselected()
fun onFragmentChanged() {}
}
interface TitledView {

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorPrimaryLight"/>
<corners android:radius="12dp"/>
<solid android:color="@color/colorPrimaryLight" />
<corners android:radius="12dp" />
</shape>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorPrimary"/>
<corners android:radius="12dp"/>
<solid android:color="@color/colorPrimary" />
<corners android:radius="12dp" />
</shape>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7,11v2h10v-2L7,11zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z" />
</vector>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/excuseReason"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/attendance_excuse_dialog_reason" />
<requestFocus />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -40,16 +40,16 @@
<TextView
android:id="@+id/gradeDialogColorAndWeightValue"
android:layout_width="match_parent"
android:layout_marginTop="2dp"
android:minHeight="32dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="2dp"
android:background="@color/grade_black"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="14sp"
android:textIsSelectable="true"
android:maxLines="2"
android:minHeight="32dp"
android:textColor="@android:color/white"
android:textIsSelectable="true"
android:textSize="14sp"
tools:text="Waga: 1.00" />
</LinearLayout>
@ -57,16 +57,17 @@
android:id="@+id/gradeDialogHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="120dp"
android:gravity="center_vertical"
android:layout_toLeftOf="@+id/gradeDialogValueLayout"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:layout_toStartOf="@+id/gradeDialogValueLayout"
android:layout_toLeftOf="@+id/gradeDialogValueLayout"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:gravity="center_vertical"
android:minHeight="120dp"
android:orientation="vertical">
<TextView
android:id="@+id/gradeDialogSubject"
android:layout_width="wrap_content"

View File

@ -56,6 +56,20 @@
android:textSize="20sp" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/attendanceExcuseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:clickable="true"
android:focusable="true"
android:tint="?colorOnSecondary"
android:visibility="gone"
android:text="@string/attendance_excuse_title"
app:icon="@drawable/ic_all_done_all"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/attendanceError"
android:layout_width="match_parent"

View File

@ -1,5 +1,4 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/gradeHeaderContainer"
android:layout_width="match_parent"
@ -53,15 +52,15 @@
<TextView
android:id="@+id/gradeHeaderNote"
android:layout_width="wrap_content"
android:minWidth="20dp"
android:layout_height="20dp"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:background="@drawable/background_header_note"
android:gravity="center"
android:minWidth="20dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:gravity="center"
android:paddingLeft="5dp"
android:layout_marginTop="10dp"
android:background="@drawable/background_header_note"
android:paddingRight="5dp"
android:textColor="?colorOnPrimary"
android:textSize="14sp"
tools:text="255" />

View File

@ -13,16 +13,40 @@
android:paddingBottom="7dp"
tools:context=".ui.modules.attendance.AttendanceItem">
<TextView
android:id="@+id/attendanceItemNumber"
<LinearLayout
android:id="@+id/attendanceItemNumberContainer"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:gravity="center"
android:includeFontPadding="false"
android:maxLength="2"
android:textSize="32sp"
tools:text="5" />
android:orientation="vertical">
<TextView
android:id="@+id/attendanceItemNumber"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:includeFontPadding="false"
android:maxLength="2"
android:textSize="32sp"
android:visibility="gone"
tools:visibility="visible"
tools:text="5" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/attendanceItemExcuseCheckbox"
android:layout_width="32dp"
android:layout_height="32dp"
android:text="@null"
android:visibility="gone" />
<ImageView
android:id="@+id/attendanceItemExcuseInfo"
android:layout_width="32dp"
android:layout_height="32dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_excuse_waiting"
app:tint="?attr/colorOnSurface" />
</LinearLayout>
<TextView
android:id="@+id/attendanceItemSubject"
@ -35,8 +59,8 @@
android:layout_marginRight="40dp"
android:layout_toStartOf="@id/attendanceItemAlert"
android:layout_toLeftOf="@id/attendanceItemAlert"
android:layout_toEndOf="@+id/attendanceItemNumber"
android:layout_toRightOf="@+id/attendanceItemNumber"
android:layout_toEndOf="@+id/attendanceItemNumberContainer"
android:layout_toRightOf="@+id/attendanceItemNumberContainer"
android:ellipsize="end"
android:maxLines="1"
android:textSize="17sp"
@ -48,7 +72,7 @@
android:layout_height="wrap_content"
android:layout_alignStart="@id/attendanceItemSubject"
android:layout_alignLeft="@id/attendanceItemSubject"
android:layout_alignBottom="@+id/attendanceItemNumber"
android:layout_alignBottom="@+id/attendanceItemNumberContainer"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/excuseMenuSubmit"
android:icon="@drawable/ic_all_done"
android:title="@string/attendance_excuse_dialog_submit"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="always" />
</menu>

View File

@ -141,6 +141,11 @@
<item quantity="many">%1$d nieobecności</item>
<item quantity="other">%1$d nieobecności</item>
</plurals>
<string name="attendance_excuse_dialog_reason">Powód nieobecności (opcjonalny)</string>
<string name="attendance_excuse_dialog_submit">Wyślij</string>
<string name="attendance_excuse_success">Usprawiedliwiono pomyślnie!</string>
<string name="attendance_excuse_no_selection">Musisz wybrać co najmniej jedną nieobecność!</string>
<string name="attendance_excuse_title">Usprawiedliw</string>
<!--Attendance summary-->

View File

@ -135,6 +135,11 @@
<item quantity="one">%1$d absence</item>
<item quantity="other">%1$d absences</item>
</plurals>
<string name="attendance_excuse_dialog_reason">Absence reason (optional)</string>
<string name="attendance_excuse_dialog_submit">Send</string>
<string name="attendance_excuse_success">Absence excused successfully!</string>
<string name="attendance_excuse_no_selection">You must select at least one absence!</string>
<string name="attendance_excuse_title">Excuse</string>
<!--Attendance summary-->

View File

@ -16,6 +16,7 @@
<style name="WulkanowyTheme.NoActionBar" parent="WulkanowyTheme">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="windowActionModeOverlay">true</item>
</style>
<style name="WulkanowyTheme.SplashScreen" parent="WulkanowyTheme.NoActionBar">