Skip to content

Commit

Permalink
Titania report enhancement
Browse files Browse the repository at this point in the history
- fix incoming Titania dataset validation
- add possibility to remove individual rows from Titania error report
  • Loading branch information
msavolainen-gofore committed Dec 16, 2024
1 parent 63b8307 commit 4560059
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 49 deletions.
73 changes: 43 additions & 30 deletions frontend/src/employee-frontend/components/reports/TitaniaErrors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from 'lib-common/generated/api-types/reports'
import { useQueryResult } from 'lib-common/query'
import Title from 'lib-components/atoms/Title'
import { MutateButton } from 'lib-components/atoms/buttons/MutateButton'
import ReturnButton from 'lib-components/atoms/buttons/ReturnButton'
import { Container, ContentArea } from 'lib-components/layout/Container'
import { Table, Thead, Th, Tbody, Td, Tr } from 'lib-components/layout/Table'
Expand All @@ -20,7 +21,7 @@ import { Gap } from 'lib-components/white-space'
import { useTranslation } from '../../state/i18n'
import { renderResult } from '../async-rendering'

import { titaniaErrorsReportQuery } from './queries'
import { clearTitaniaErrorMutation, titaniaErrorsReportQuery } from './queries'

export default React.memo(function TitaniaErrors() {
const { i18n } = useTranslation()
Expand All @@ -36,54 +37,66 @@ export default React.memo(function TitaniaErrors() {
{renderResult(titaniaErrorsResult, (rows) => (
<>
{rows.map((row: TitaniaErrorReportRow) => (
<>
<H1 key={row.requestTime.format()}>
<div key={row.requestTime.format()}>
<H1>
{i18n.reports.titaniaErrors.header +
' ' +
row.requestTime.format()}
</H1>
{row.units.map((unit: TitaniaErrorUnit) => (
<>
<H2 key={unit.unitName}>{unit.unitName}</H2>
<div key={unit.unitName}>
<H2>{unit.unitName}</H2>
{unit.employees.map((employee: TitaniaErrorEmployee) => (
<>
<H3 key={employee.employeeName}>
<div key={employee.employeeName}>
<H3>
{employee.employeeName +
(employee.employeeNumber == ''
? ''
: ' (' + employee.employeeNumber + ')')}
</H3>
<Table>
<Thead>
<Th>{i18n.reports.titaniaErrors.date}</Th>
<Th>{i18n.reports.titaniaErrors.shift1}</Th>
<Th>{i18n.reports.titaniaErrors.shift2}</Th>
<Tr>
<Th>{i18n.reports.titaniaErrors.date}</Th>
<Th>{i18n.reports.titaniaErrors.shift1}</Th>
<Th>{i18n.reports.titaniaErrors.shift2}</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{employee.conflictingShifts.map(
(conflict, index) => (
<Tr key={index}>
<Td>{conflict.shiftDate.format()}</Td>
<Td>
{conflict.shiftBegins.format() +
' - ' +
conflict.shiftEnds.format()}
</Td>
<Td>
{conflict.overlappingShiftBegins.format() +
' - ' +
conflict.overlappingShiftEnds.format()}
</Td>
</Tr>
)
)}
{employee.conflictingShifts.map((conflict) => (
<Tr key={conflict.id}>
<Td>{conflict.shiftDate.format()}</Td>
<Td>
{conflict.shiftBegins.format() +
' - ' +
conflict.shiftEnds.format()}
</Td>
<Td>
{conflict.overlappingShiftBegins.format() +
' - ' +
conflict.overlappingShiftEnds.format()}
</Td>
<Td>
<MutateButton
primary
text={i18n.common.remove}
mutation={clearTitaniaErrorMutation}
onClick={() => ({
conflictId: conflict.id
})}
data-qa={`delete-button-${conflict.id}`}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</>
</div>
))}
</>
</div>
))}
</>
</div>
))}
</>
))}
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/employee-frontend/components/reports/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Arg0, UUID } from 'lib-common/types'

import { sendJamixOrders } from '../../generated/api-clients/jamix'
import {
clearTitaniaErrors,
getAssistanceNeedsAndActionsReport,
getAssistanceNeedsAndActionsReportByChild,
getAttendanceReservationReportByChild,
Expand Down Expand Up @@ -253,6 +254,11 @@ export const titaniaErrorsReportQuery = query({
queryKey: queryKeys.titaniaErrorsReport
})

export const clearTitaniaErrorMutation = mutation({
api: clearTitaniaErrors,
invalidateQueryKeys: () => [queryKeys.titaniaErrorsReport()]
})

export const incompleteIncomeReportQuery = query({
api: getIncompleteIncomeReport,
queryKey: queryKeys.incompleteIncomeReport
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/employee-frontend/generated/api-clients/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { SextetReportRow } from 'lib-common/generated/api-types/reports'
import { SourceUnitsReportRow } from 'lib-common/generated/api-types/reports'
import { StartingPlacementsRow } from 'lib-common/generated/api-types/reports'
import { TitaniaErrorReportRow } from 'lib-common/generated/api-types/reports'
import { TitaniaErrorsId } from 'lib-common/generated/api-types/shared'
import { UnitsReportRow } from 'lib-common/generated/api-types/reports'
import { VardaChildErrorReportRow } from 'lib-common/generated/api-types/reports'
import { VardaUnitErrorReportRow } from 'lib-common/generated/api-types/reports'
Expand Down Expand Up @@ -1021,6 +1022,22 @@ export async function getStartingPlacementsReport(
}


/**
* Generated from fi.espoo.evaka.reports.TitaniaErrorReport.clearTitaniaErrors
*/
export async function clearTitaniaErrors(
request: {
conflictId: TitaniaErrorsId
}
): Promise<void> {
const { data: json } = await client.request<JsonOf<void>>({
url: uri`/employee/reports/titania-errors/${request.conflictId}`.toString(),
method: 'DELETE'
})
return json
}


/**
* Generated from fi.espoo.evaka.reports.TitaniaErrorReport.getTitaniaErrorsReport
*/
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib-common/generated/api-types/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { PreschoolAssistanceLevel } from './assistance'
import { ProviderType } from './daycare'
import { ServiceNeedId } from './shared'
import { ServiceNeedOption } from './application'
import { TitaniaErrorsId } from './shared'
import { UUID } from '../../types'
import { VoucherValueDecisionId } from './shared'

Expand Down Expand Up @@ -954,6 +955,7 @@ export interface StartingPlacementsRow {
* Generated from fi.espoo.evaka.reports.TitaniaErrorConflict
*/
export interface TitaniaErrorConflict {
id: TitaniaErrorsId
overlappingShiftBegins: LocalTime
overlappingShiftEnds: LocalTime
shiftBegins: LocalTime
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib-common/generated/api-types/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ export type StaffAttendanceExternalId = string

export type StaffAttendanceRealtimeId = string

export type TitaniaErrorsId = Id<'TitaniaErrors'>

/**
* Generated from fi.espoo.evaka.shared.domain.Translatable
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package fi.espoo.evaka.reports

import fi.espoo.evaka.Audit
import fi.espoo.evaka.shared.TitaniaConflictId
import fi.espoo.evaka.shared.auth.AuthenticatedUser
import fi.espoo.evaka.shared.db.Database
import fi.espoo.evaka.shared.domain.EvakaClock
Expand All @@ -13,7 +14,9 @@ import fi.espoo.evaka.shared.security.AccessControl
import fi.espoo.evaka.shared.security.Action
import java.time.LocalDate
import java.time.LocalTime
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController

@RestController
Expand All @@ -38,6 +41,27 @@ class TitaniaErrorReport(private val accessControl: AccessControl) {
}
.also { Audit.TitaniaReportRead.log() }
}

@DeleteMapping("/employee/reports/titania-errors/{conflictId}")
fun clearTitaniaErrors(
db: Database,
user: AuthenticatedUser.Employee,
clock: EvakaClock,
@PathVariable conflictId: TitaniaConflictId,
) {
return db.connect { dbc ->
dbc.transaction { tx ->
accessControl.requirePermissionFor(
tx,
user,
clock,
Action.Global.READ_TITANIA_ERRORS,
)
tx.deleteTitaniaError(conflictId)
}
}
.also { Audit.TitaniaReportRead.log() }
}
}

fun Database.Read.getTitaniaErrors(): List<TitaniaErrorReportRow> {
Expand All @@ -49,7 +73,8 @@ fun Database.Read.getTitaniaErrors(): List<TitaniaErrorReportRow> {
te.request_time,
emp.first_name,
emp.last_name,
emp.employee_number,
emp.employee_number,
te.id,
te.shift_date,
te.shift_begins,
te.shift_ends,
Expand Down Expand Up @@ -100,6 +125,7 @@ fun Database.Read.getTitaniaErrors(): List<TitaniaErrorReportRow> {
employeeEntry.value[0].employeeNumber ?: "",
employeeEntry.value.map { shiftEntry ->
TitaniaErrorConflict(
shiftEntry.id,
shiftEntry.shiftDate,
shiftEntry.shiftBegins,
shiftEntry.shiftEnds,
Expand All @@ -115,11 +141,29 @@ fun Database.Read.getTitaniaErrors(): List<TitaniaErrorReportRow> {
}
}

fun Database.Transaction.deleteTitaniaErrors() {
createUpdate { sql("DELETE FROM titania_errors") }.execute()
}

fun Database.Transaction.deleteTitaniaError(id: TitaniaConflictId) {
createUpdate {
sql(
"""
DELETE FROM titania_errors
WHERE id = ${bind(id)}
"""
.trimIndent()
)
}
.execute()
}

data class TitaniaDbRow(
val requestTime: HelsinkiDateTime,
val firstName: String,
val lastName: String,
val employeeNumber: String?,
val id: TitaniaConflictId,
val shiftDate: LocalDate,
val shiftBegins: LocalTime,
val shiftEnds: LocalTime,
Expand All @@ -129,6 +173,7 @@ data class TitaniaDbRow(
)

data class TitaniaErrorConflict(
val id: TitaniaConflictId,
val shiftDate: LocalDate,
val shiftBegins: LocalTime,
val shiftEnds: LocalTime,
Expand Down
4 changes: 4 additions & 0 deletions service/src/main/kotlin/fi/espoo/evaka/shared/Id.kt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ sealed interface DatabaseTable {

sealed class StaffOccupancyCoefficient : DatabaseTable

sealed class TitaniaErrors : DatabaseTable

sealed class VoucherValueDecision : DatabaseTable
}

Expand Down Expand Up @@ -344,6 +346,8 @@ typealias StaffAttendanceRealtimeId = Id<DatabaseTable.StaffAttendanceRealtime>

typealias StaffOccupancyCoefficientId = Id<DatabaseTable.StaffOccupancyCoefficient>

typealias TitaniaConflictId = Id<DatabaseTable.TitaniaErrors>

typealias VoucherValueDecisionId = Id<DatabaseTable.VoucherValueDecision>

@JsonSerialize(converter = Id.ToJson::class)
Expand Down
26 changes: 8 additions & 18 deletions service/src/main/kotlin/fi/espoo/evaka/titania/TitaniaService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import fi.espoo.evaka.shared.EmployeeId
import fi.espoo.evaka.shared.db.Database
import fi.espoo.evaka.shared.domain.HelsinkiDateTime
import fi.espoo.evaka.shared.domain.HelsinkiDateTimeRange
import fi.espoo.evaka.shared.domain.TimeRange
import java.time.Duration
import java.time.LocalTime
import java.time.format.DateTimeFormatter
Expand Down Expand Up @@ -62,7 +61,7 @@ class TitaniaService(private val idConverter: TitaniaEmployeeIdConverter) {
val employeeNumbers = persons.map { (employeeNumber, _) -> employeeNumber }.distinct()
val employeeNumberToId = tx.getEmployeeIdsByNumbers(employeeNumbers)

var unmergedSameDayPlans = mutableListOf<StaffAttendancePlan>()
var unmergedPlans = mutableListOf<StaffAttendancePlan>()
val overlappingShifts = mutableListOf<TitaniaOverLappingShifts>()

val newPlans =
Expand Down Expand Up @@ -120,26 +119,17 @@ class TitaniaService(private val idConverter: TitaniaEmployeeIdConverter) {
plans.add(next)
}

if (
unmergedSameDayPlans.lastOrNull()?.startTime?.toLocalDate() !=
next.startTime.toLocalDate()
) {
unmergedSameDayPlans = mutableListOf(next)
if (unmergedPlans.isEmpty()) {
unmergedPlans = mutableListOf(next)
} else {
// identical shifts are deduplicated later, ignore them here
if (next !in unmergedSameDayPlans) {
unmergedSameDayPlans
if (next !in unmergedPlans) {
unmergedPlans
.filter { it.employeeId == next.employeeId }
.filter {
TimeRange(
next.startTime.toLocalTime(),
next.endTime.toLocalTime(),
)
HelsinkiDateTimeRange(next.startTime, next.endTime)
.overlaps(
TimeRange(
it.startTime.toLocalTime(),
it.endTime.toLocalTime(),
)
HelsinkiDateTimeRange(it.startTime, it.endTime)
)
}
.forEach {
Expand All @@ -155,7 +145,7 @@ class TitaniaService(private val idConverter: TitaniaEmployeeIdConverter) {
)
}

unmergedSameDayPlans.add(next)
unmergedPlans.add(next)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE titania_errors
ADD COLUMN id uuid DEFAULT ext.uuid_generate_v1mc();

ALTER TABLE titania_errors
ADD CONSTRAINT titania_errors_pkey PRIMARY KEY (id);
1 change: 1 addition & 0 deletions service/src/main/resources/migrations.txt
Original file line number Diff line number Diff line change
Expand Up @@ -472,3 +472,4 @@ V473__person_municipality_of_residence.sql
V474__holiday_questionnaire_open_ranges.sql
V475__application_modified_metadata.sql
V476__finance_metadata_process.sql
V477__titania_errors_id.sql

0 comments on commit 4560059

Please sign in to comment.