1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 00:39: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 @Test
fun saveAndReadTest() { fun saveAndReadTest() {
attendanceLocal.saveAttendance(listOf( attendanceLocal.saveAttendance(listOf(
Attendance(1, 2, LocalDate.of(2018, 9, 10), 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, LocalDate.of(2018, 9, 14), 0, "", "", false, false, false, false, false, false), Attendance(1, 2, 3, LocalDate.of(2018, 9, 14), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.WAITING.name),
Attendance(1, 2, LocalDate.of(2018, 9, 17), 0, "", "", false, false, false, false, false, false) Attendance(1, 2, 3, LocalDate.of(2018, 9, 17), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name)
)) ))
val attendance = attendanceLocal 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
import org.threeten.bp.LocalDateTime.now 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.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 { fun createTimetableLocal(start: LocalDateTime, number: Int, room: String = "", subject: String = "", teacher: String = "", changes: Boolean = false): TimetableLocal {
return 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.Migration19
import io.github.wulkanowy.data.db.migrations.Migration2 import io.github.wulkanowy.data.db.migrations.Migration2
import io.github.wulkanowy.data.db.migrations.Migration20 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.Migration3
import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration5
@ -102,7 +103,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 20 const val VERSION_SCHEMA = 21
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> { fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf( return arrayOf(
@ -124,7 +125,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration17(), Migration17(),
Migration18(), Migration18(),
Migration19(sharedPrefProvider), Migration19(sharedPrefProvider),
Migration20() Migration20(),
Migration21()
) )
} }

View File

@ -15,6 +15,9 @@ data class Attendance(
@ColumnInfo(name = "diary_id") @ColumnInfo(name = "diary_id")
val diaryId: Int, val diaryId: Int,
@ColumnInfo(name = "time_id")
val timeId: Int,
val date: LocalDate, val date: LocalDate,
val number: Int, val number: Int,
@ -33,7 +36,13 @@ data class Attendance(
val excused: Boolean, val excused: Boolean,
val deleted: Boolean val deleted: Boolean,
val excusable: Boolean,
@ColumnInfo(name = "excuse_status")
val excuseStatus: String?
) : Serializable { ) : Serializable {
@PrimaryKey(autoGenerate = true) @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.Attendance
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Absent
import io.reactivex.Single import io.reactivex.Single
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalTime
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -19,6 +22,7 @@ class AttendanceRemote @Inject constructor(private val sdk: Sdk) {
studentId = semester.studentId, studentId = semester.studentId,
diaryId = semester.diaryId, diaryId = semester.diaryId,
date = it.date, date = it.date,
timeId = it.timeId,
number = it.number, number = it.number,
subject = it.subject, subject = it.subject,
name = it.name, name = it.name,
@ -27,9 +31,20 @@ class AttendanceRemote @Inject constructor(private val sdk: Sdk) {
exemption = it.exemption, exemption = it.exemption,
lateness = it.lateness, lateness = it.lateness,
excused = it.excused, 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 } } }).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 package io.github.wulkanowy.ui.modules.attendance
import android.content.DialogInterface.BUTTON_POSITIVE
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
@ -10,8 +11,9 @@ import android.view.View.GONE
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem 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.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.setOnItemClickListener import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.dialog_excuse.*
import kotlinx.android.synthetic.main.fragment_attendance.* import kotlinx.android.synthetic.main.fragment_attendance.*
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -35,7 +38,13 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
lateinit var presenter: AttendancePresenter lateinit var presenter: AttendancePresenter
@Inject @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 { companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE" 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 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -66,6 +103,7 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
override fun initView() { override fun initView() {
attendanceAdapter.setOnItemClickListener(presenter::onAttendanceItemSelected) attendanceAdapter.setOnItemClickListener(presenter::onAttendanceItemSelected)
attendanceAdapter.onExcuseCheckboxSelect = presenter::onExcuseCheckboxSelect
with(attendanceRecycler) { with(attendanceRecycler) {
layoutManager = SmoothScrollLinearLayoutManager(context) layoutManager = SmoothScrollLinearLayoutManager(context)
@ -83,6 +121,8 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
attendanceNavDate.setOnClickListener { presenter.onPickDate() } attendanceNavDate.setOnClickListener { presenter.onPickDate() }
attendanceNextButton.setOnClickListener { presenter.onNextDay() } attendanceNextButton.setOnClickListener { presenter.onNextDay() }
attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() }
attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f)) attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f))
} }
@ -115,6 +155,10 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
if (::presenter.isInitialized) presenter.onViewReselected() if (::presenter.isInitialized) presenter.onViewReselected()
} }
override fun onFragmentChanged() {
if (::presenter.isInitialized) presenter.onMainViewChanged()
}
override fun popView() { override fun popView() {
(activity as? MainActivity)?.popView() (activity as? MainActivity)?.popView()
} }
@ -155,6 +199,10 @@ class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MainChildVie
attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE
} }
override fun showExcuseButton(show: Boolean) {
attendanceExcuseButton.visibility = if (show) VISIBLE else GONE
}
override fun showAttendanceDialog(lesson: Attendance) { override fun showAttendanceDialog(lesson: Attendance) {
(activity as? MainActivity)?.showDialogFragment(AttendanceDialog.newInstance(lesson)) (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() { override fun openSummaryView() {
(activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance()) (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) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())

View File

@ -1,18 +1,22 @@
package io.github.wulkanowy.ui.modules.attendance package io.github.wulkanowy.ui.modules.attendance
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import androidx.core.view.isVisible
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.repositories.attendance.SentExcuseStatus
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_attendance.* 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 override fun getLayoutRes() = R.layout.item_attendance
@ -26,6 +30,34 @@ class AttendanceItem(val attendance: Attendance) : AbstractFlexibleItem<Attendan
attendanceItemSubject.text = attendance.subject attendanceItemSubject.text = attendance.subject
attendanceItemDescription.text = attendance.name attendanceItemDescription.text = attendance.name
attendanceItemAlert.visibility = attendance.run { if (absence && !excused) VISIBLE else INVISIBLE } 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 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 override val containerView: View
get() = contentView 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 android.annotation.SuppressLint
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem 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.attendance.AttendanceRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository
@ -40,6 +41,8 @@ class AttendancePresenter @Inject constructor(
private lateinit var lastError: Throwable private lateinit var lastError: Throwable
private val attendanceToExcuseList = mutableListOf<Attendance>()
fun onAttachView(view: AttendanceView, date: Long?) { fun onAttachView(view: AttendanceView, date: Long?) {
super.onAttachView(view) super.onAttachView(view)
view.initView() view.initView()
@ -51,11 +54,15 @@ class AttendancePresenter @Inject constructor(
} }
fun onPreviousDay() { fun onPreviousDay() {
view?.finishActionMode()
attendanceToExcuseList.clear()
loadData(currentDate.previousSchoolDay) loadData(currentDate.previousSchoolDay)
reloadView() reloadView()
} }
fun onNextDay() { fun onNextDay() {
view?.finishActionMode()
attendanceToExcuseList.clear()
loadData(currentDate.nextSchoolDay) loadData(currentDate.nextSchoolDay)
reloadView() reloadView()
} }
@ -100,10 +107,59 @@ class AttendancePresenter @Inject constructor(
} }
} }
fun onMainViewChanged() {
view?.finishActionMode()
}
fun onAttendanceItemSelected(item: AbstractFlexibleItem<*>?) { fun onAttendanceItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is AttendanceItem) { view?.apply {
Timber.i("Select attendance item ${item.attendance.id}") if (item is AttendanceItem && !excuseActionMode) {
view?.showAttendanceDialog(item.attendance) 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()) showEmpty(it.isEmpty())
showErrorView(false) showErrorView(false)
showContent(it.isNotEmpty()) showContent(it.isNotEmpty())
showExcuseButton(it.any { item -> item.attendance.excusable })
} }
analytics.logEvent("load_attendance", "items" to it.size, "force_refresh" to forceRefresh) 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) { private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run { view?.run {
if (isViewEmpty) { if (isViewEmpty) {

View File

@ -10,6 +10,12 @@ interface AttendanceView : BaseView {
val currentStackSize: Int? val currentStackSize: Int?
val excuseSuccessString: String
val excuseNoSelectionString: String
val excuseActionMode: Boolean
fun initView() fun initView()
fun updateData(data: List<AttendanceItem>) fun updateData(data: List<AttendanceItem>)
@ -38,11 +44,21 @@ interface AttendanceView : BaseView {
fun showNextButton(show: Boolean) fun showNextButton(show: Boolean)
fun showExcuseButton(show: Boolean)
fun showAttendanceDialog(lesson: Attendance) fun showAttendanceDialog(lesson: Attendance)
fun showDatePickerDialog(currentDate: LocalDate) fun showDatePickerDialog(currentDate: LocalDate)
fun showExcuseDialog()
fun openSummaryView() fun openSummaryView()
fun startActionMode()
fun showExcuseCheckboxes(show: Boolean)
fun finishActionMode()
fun popView() 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.safelyPopFragments
import io.github.wulkanowy.utils.setOnViewChangeListener import io.github.wulkanowy.utils.setOnViewChangeListener
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MainActivity : BaseActivity<MainPresenter>(), MainView { class MainActivity : BaseActivity<MainPresenter>(), MainView {
@ -167,6 +168,11 @@ class MainActivity : BaseActivity<MainPresenter>(), MainView {
(navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentReselected() (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) { fun showDialogFragment(dialog: DialogFragment) {
navController.showDialogFragment(dialog) 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.about.license.LicenseModule
import io.github.wulkanowy.ui.modules.account.AccountDialog import io.github.wulkanowy.ui.modules.account.AccountDialog
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment 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.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment
@ -51,7 +52,7 @@ abstract class MainModule {
} }
@PerFragment @PerFragment
@ContributesAndroidInjector @ContributesAndroidInjector(modules = [AttendanceModule::class])
abstract fun bindAttendanceFragment(): AttendanceFragment abstract fun bindAttendanceFragment(): AttendanceFragment
@PerFragment @PerFragment

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorPrimary"/> <solid android:color="@color/colorPrimary" />
<corners android:radius="12dp"/> <corners android:radius="12dp" />
</shape> </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 <TextView
android:id="@+id/gradeDialogColorAndWeightValue" android:id="@+id/gradeDialogColorAndWeightValue"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_marginTop="2dp"
android:minHeight="32dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginTop="2dp"
android:background="@color/grade_black" android:background="@color/grade_black"
android:gravity="center" android:gravity="center"
android:textColor="@android:color/white"
android:textSize="14sp"
android:textIsSelectable="true"
android:maxLines="2" android:maxLines="2"
android:minHeight="32dp"
android:textColor="@android:color/white"
android:textIsSelectable="true"
android:textSize="14sp"
tools:text="Waga: 1.00" /> tools:text="Waga: 1.00" />
</LinearLayout> </LinearLayout>
@ -57,16 +57,17 @@
android:id="@+id/gradeDialogHeader" android:id="@+id/gradeDialogHeader"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="120dp" android:layout_marginEnd="12dp"
android:gravity="center_vertical" android:layout_marginRight="12dp"
android:layout_toLeftOf="@+id/gradeDialogValueLayout"
android:layout_toStartOf="@+id/gradeDialogValueLayout" android:layout_toStartOf="@+id/gradeDialogValueLayout"
android:layout_toLeftOf="@+id/gradeDialogValueLayout"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:layout_marginRight="12dp" android:gravity="center_vertical"
android:layout_marginEnd="12dp" android:minHeight="120dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/gradeDialogSubject" android:id="@+id/gradeDialogSubject"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -56,6 +56,20 @@
android:textSize="20sp" /> android:textSize="20sp" />
</LinearLayout> </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 <LinearLayout
android:id="@+id/attendanceError" android:id="@+id/attendanceError"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

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

View File

@ -13,16 +13,40 @@
android:paddingBottom="7dp" android:paddingBottom="7dp"
tools:context=".ui.modules.attendance.AttendanceItem"> tools:context=".ui.modules.attendance.AttendanceItem">
<TextView <LinearLayout
android:id="@+id/attendanceItemNumber" android:id="@+id/attendanceItemNumberContainer"
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_centerVertical="true"
android:gravity="center" android:gravity="center"
android:includeFontPadding="false" android:orientation="vertical">
android:maxLength="2"
android:textSize="32sp" <TextView
tools:text="5" /> 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 <TextView
android:id="@+id/attendanceItemSubject" android:id="@+id/attendanceItemSubject"
@ -35,8 +59,8 @@
android:layout_marginRight="40dp" android:layout_marginRight="40dp"
android:layout_toStartOf="@id/attendanceItemAlert" android:layout_toStartOf="@id/attendanceItemAlert"
android:layout_toLeftOf="@id/attendanceItemAlert" android:layout_toLeftOf="@id/attendanceItemAlert"
android:layout_toEndOf="@+id/attendanceItemNumber" android:layout_toEndOf="@+id/attendanceItemNumberContainer"
android:layout_toRightOf="@+id/attendanceItemNumber" android:layout_toRightOf="@+id/attendanceItemNumberContainer"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textSize="17sp" android:textSize="17sp"
@ -48,7 +72,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignStart="@id/attendanceItemSubject" android:layout_alignStart="@id/attendanceItemSubject"
android:layout_alignLeft="@id/attendanceItemSubject" android:layout_alignLeft="@id/attendanceItemSubject"
android:layout_alignBottom="@+id/attendanceItemNumber" android:layout_alignBottom="@+id/attendanceItemNumberContainer"
android:maxLines="1" android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="12sp" 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="many">%1$d nieobecności</item>
<item quantity="other">%1$d nieobecności</item> <item quantity="other">%1$d nieobecności</item>
</plurals> </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--> <!--Attendance summary-->

View File

@ -135,6 +135,11 @@
<item quantity="one">%1$d absence</item> <item quantity="one">%1$d absence</item>
<item quantity="other">%1$d absences</item> <item quantity="other">%1$d absences</item>
</plurals> </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--> <!--Attendance summary-->

View File

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