From b6c82d9de146daaff1f67b91954a103a23dc6b45 Mon Sep 17 00:00:00 2001 From: Tuukka Kataja Date: Mon, 2 Oct 2023 11:27:11 +0300 Subject: [PATCH] Various SAP reporting fixes - #168 Fix "Kohdistamaton" - #177 show 0e instead of empty cells - #191 Fix "toimipiste" filter - #170 Allow filtering empty (db NULL) --- backend/src/components/code/index.ts | 25 +++++++++-- .../components/sap/environmentCodeReport.ts | 43 +++++++++++++------ backend/src/router/code.ts | 2 +- backend/src/router/sapReport.ts | 4 +- frontend/src/components/forms/CodeSelect.tsx | 17 ++++++-- .../SapReports/BlanketContractReport.tsx | 6 +-- .../SapReports/EnvironmentalCodeReport.tsx | 12 ++++-- .../EnvironmentalCodeReportFilters.tsx | 12 +++++- shared/src/language/fi.json | 1 + shared/src/schema/code.ts | 5 +++ 10 files changed, 96 insertions(+), 31 deletions(-) diff --git a/backend/src/components/code/index.ts b/backend/src/components/code/index.ts index 12e744c8..021f6b87 100644 --- a/backend/src/components/code/index.ts +++ b/backend/src/components/code/index.ts @@ -2,7 +2,7 @@ import { sql } from 'slonik'; import { getPool } from '@backend/db'; -import { Code, CodeId, codeSchema } from '@shared/schema/code'; +import { Code, CodeId, EXPLICIT_EMPTY, codeSchema } from '@shared/schema/code'; const codeSelectFragment = sql.fragment` SELECT @@ -17,11 +17,30 @@ const codeSelectFragment = sql.fragment` FROM app.code `; -export async function getCodesForCodeList(codeListId: Code['id']['codeListId']) { - return getPool().any(sql.type(codeSchema)` +export async function getCodesForCodeList( + codeListId: Code['id']['codeListId'], + emptySelection: boolean = false +) { + const results = await getPool().any(sql.type(codeSchema)` ${codeSelectFragment} WHERE (code.id).code_list_id = ${codeListId} `); + + return emptySelection + ? [ + { + id: { + id: EXPLICIT_EMPTY, + codeListId, + }, + text: { + fi: 'Tyhjä arvo', + en: 'Empty value', + }, + }, + ...results, + ] + : results; } export function codeIdFragment( diff --git a/backend/src/components/sap/environmentCodeReport.ts b/backend/src/components/sap/environmentCodeReport.ts index d2ce86f8..72d3ecf5 100644 --- a/backend/src/components/sap/environmentCodeReport.ts +++ b/backend/src/components/sap/environmentCodeReport.ts @@ -2,8 +2,33 @@ import { z } from 'zod'; import { getPool, sql, textToTsQuery } from '@backend/db'; +import { EXPLICIT_EMPTY } from '@shared/schema/code'; import { EnvironmentCodeReportQuery, environmentCodeReportSchema } from '@shared/schema/sapReport'; +function filterPlantFragment(plants?: EnvironmentCodeReportQuery['filters']['plants']) { + if (!plants || plants.length === 0) return sql.fragment`true`; + + const includeEmpty = plants.includes(EXPLICIT_EMPTY); + const inArrayFragment = sql.fragment`plant = ANY(${sql.array(plants, 'text')})`; + return includeEmpty ? sql.fragment`(${inArrayFragment} OR plant IS NULL)` : inArrayFragment; +} + +function filterReasonForEnvironmentalInvestmentFragment( + reasonsForEnvironmentalInvestment?: EnvironmentCodeReportQuery['filters']['reasonsForEnvironmentalInvestment'] +) { + if (!reasonsForEnvironmentalInvestment || reasonsForEnvironmentalInvestment.length === 0) + return sql.fragment`true`; + + const includeEmpty = reasonsForEnvironmentalInvestment.includes(EXPLICIT_EMPTY); + const inArrayFragment = sql.fragment`"reasonForEnvironmentalInvestment" = ANY(${sql.array( + reasonsForEnvironmentalInvestment, + 'text' + )})`; + return includeEmpty + ? sql.fragment`(${inArrayFragment} OR "reasonForEnvironmentalInvestment" IS NULL)` + : inArrayFragment; +} + function environmentCodeReportFragment(params?: Partial) { const years = params?.filters?.years ?? []; return sql.fragment` @@ -60,20 +85,10 @@ function environmentCodeReportFragment(params?: Partial 0 - ? sql.fragment`plant = ANY(${sql.array(params.filters.plants, 'text')})` - : sql.fragment`true` - } - AND ${ - params?.filters?.reasonsForEnvironmentalInvestment && - params.filters.reasonsForEnvironmentalInvestment.length > 0 - ? sql.fragment`"reasonForEnvironmentalInvestment" = ANY(${sql.array( - params.filters.reasonsForEnvironmentalInvestment, - 'text' - )})` - : sql.fragment`true` - } + AND (${filterPlantFragment(params?.filters?.plants)}) + AND (${filterReasonForEnvironmentalInvestmentFragment( + params?.filters?.reasonsForEnvironmentalInvestment + )}) ${ params?.sort ? sql.fragment`ORDER BY ${sql.identifier([params.sort.key])} ${ diff --git a/backend/src/router/code.ts b/backend/src/router/code.ts index 371f2c66..81e89644 100644 --- a/backend/src/router/code.ts +++ b/backend/src/router/code.ts @@ -7,6 +7,6 @@ import { TRPC } from '.'; export const createCodeRouter = (t: TRPC) => t.router({ get: t.procedure.input(codeSearchSchema).query(async ({ input }) => { - return getCodesForCodeList(input.codeListId); + return getCodesForCodeList(input.codeListId, input.allowEmptySelection); }), }); diff --git a/backend/src/router/sapReport.ts b/backend/src/router/sapReport.ts index 06eda53c..87cb60d5 100644 --- a/backend/src/router/sapReport.ts +++ b/backend/src/router/sapReport.ts @@ -1,3 +1,4 @@ +import { EXPLICIT_EMPTY } from 'tre-hanna-shared/src/schema/code'; import { z } from 'zod'; import { @@ -83,7 +84,8 @@ export const createSapReportRouter = (t: TRPC) => WHERE plant IS NOT NULL ORDER BY plant ASC `); - return rows.map((row) => row.plant); + const plants = rows.map((row) => row.plant); + return [EXPLICIT_EMPTY, ...plants]; }), getYears: t.procedure.query(async () => { diff --git a/frontend/src/components/forms/CodeSelect.tsx b/frontend/src/components/forms/CodeSelect.tsx index 88651147..79703781 100644 --- a/frontend/src/components/forms/CodeSelect.tsx +++ b/frontend/src/components/forms/CodeSelect.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { trpc } from '@frontend/client'; import { langAtom } from '@frontend/stores/lang'; -import type { Code, CodeId } from '@shared/schema/code'; +import { type Code, type CodeId, EXPLICIT_EMPTY } from '@shared/schema/code'; import { MultiSelect } from './MultiSelect'; @@ -15,6 +15,7 @@ type Props = { onBlur?: () => void; getLabel?: (code: Code) => string; showIdInLabel?: boolean; + allowEmptySelection?: boolean; } & ( | { multiple: true; @@ -40,8 +41,12 @@ export function CodeSelect({ onBlur, showIdInLabel, maxTags, + allowEmptySelection, }: Props) { - const codes = trpc.code.get.useQuery({ codeListId }, { staleTime: 60 * 60 * 1000 }); + const codes = trpc.code.get.useQuery( + { codeListId, allowEmptySelection }, + { staleTime: 60 * 60 * 1000 } + ); const lang = useAtomValue(langAtom); function getCode(id: string) { @@ -49,7 +54,13 @@ export function CodeSelect({ } function getLabel(code: Code) { - return [showIdInLabel && code.id.id, code.text[lang]].filter(Boolean).join(' '); + // empty selection (00) label is not shown + let labelId = showIdInLabel ? code.id.id : null; + if (code.id.id === EXPLICIT_EMPTY) { + labelId = null; + } + + return [labelId, code.text[lang]].filter(Boolean).join(' '); } const selection = useMemo(() => { diff --git a/frontend/src/views/SapReports/BlanketContractReport.tsx b/frontend/src/views/SapReports/BlanketContractReport.tsx index fb0b8a51..2891673f 100644 --- a/frontend/src/views/SapReports/BlanketContractReport.tsx +++ b/frontend/src/views/SapReports/BlanketContractReport.tsx @@ -59,20 +59,20 @@ export function BlanketContractReport() { title: tr('sapReports.blanketContracts.totalDebit'), align: 'right', format(value) { - return formatCurrency(value ?? null); + return formatCurrency(value ?? 0); }, }, totalCredit: { title: tr('sapReports.blanketContracts.totalCredit'), align: 'right', format(value) { - return formatCurrency(value ?? null); + return formatCurrency(value ?? 0); }, }, totalActuals: { title: tr('sapReports.blanketContracts.totalActuals'), format(value) { - return formatCurrency(value ?? null); + return formatCurrency(value ?? 0); }, align: 'right', }, diff --git a/frontend/src/views/SapReports/EnvironmentalCodeReport.tsx b/frontend/src/views/SapReports/EnvironmentalCodeReport.tsx index 6f866afa..97ed0897 100644 --- a/frontend/src/views/SapReports/EnvironmentalCodeReport.tsx +++ b/frontend/src/views/SapReports/EnvironmentalCodeReport.tsx @@ -51,7 +51,11 @@ export function EnvironmentalCodeReport() { title: tr('sapReports.environmentCodes.companyCode'), align: 'right', format(value) { - return value && isInternalCompany(value) ? value : tr('sapReports.externalCompany'); + if (!value) { + return ''; + } else { + return value && isInternalCompany(value) ? value : tr('sapReports.externalCompany'); + } }, }, companyCodeText: { @@ -65,21 +69,21 @@ export function EnvironmentalCodeReport() { title: tr('sapReports.environmentCodes.totalDebit'), align: 'right', format(value) { - return formatCurrency(value ?? null); + return formatCurrency(value ?? 0); }, }, totalCredit: { title: tr('sapReports.environmentCodes.totalCredit'), align: 'right', format(value) { - return formatCurrency(value ?? null); + return formatCurrency(value ?? 0); }, }, totalActuals: { title: tr('sapReports.environmentCodes.totalActuals'), align: 'right', format(value) { - return formatCurrency(value); + return formatCurrency(value ?? 0); }, }, }} diff --git a/frontend/src/views/SapReports/EnvironmentalCodeReportFilters.tsx b/frontend/src/views/SapReports/EnvironmentalCodeReportFilters.tsx index 1ee67a59..4df3c4a3 100644 --- a/frontend/src/views/SapReports/EnvironmentalCodeReportFilters.tsx +++ b/frontend/src/views/SapReports/EnvironmentalCodeReportFilters.tsx @@ -25,6 +25,8 @@ import { yearsAtom, } from '@frontend/stores/sapReport/environmentalCodeReportFilters'; +import { EXPLICIT_EMPTY } from '@shared/schema/code'; + import { ReportSummary } from './ReportSummary'; export function EnvironmentalCodeReportFilters() { @@ -32,7 +34,7 @@ export function EnvironmentalCodeReportFilters() { const notify = useNotifications(); const [text, setText] = useAtom(textAtom); - const [plants, setPlants] = useAtom(plantsAtom); + const [plants, setPlants] = useAtom(plantsAtom); const [reasonsForEnvironmentalInvestment, setReasonsForEnvironmentalInvestment] = useAtom( reasonsForEnvironmentalInvestmentAtom ); @@ -94,16 +96,22 @@ export function EnvironmentalCodeReportFilters() { onChange={setReasonsForEnvironmentalInvestment} maxTags={1} showIdInLabel + allowEmptySelection /> {tr('sapReports.environmentCodes.plant')} - id="plants" options={allPlants ?? []} + // use the plant itself as id + getOptionId={(opt) => opt} loading={allPlantsLoading} value={plants ?? []} onChange={setPlants} + getOptionLabel={(opt) => + opt === EXPLICIT_EMPTY ? tr('sapReports.explicitEmpty') : opt + } multiple maxTags={3} /> diff --git a/shared/src/language/fi.json b/shared/src/language/fi.json index a2209ba2..7ca92981 100644 --- a/shared/src/language/fi.json +++ b/shared/src/language/fi.json @@ -371,6 +371,7 @@ "sapReports.downloadReport": "Lataa raportti", "sapReports.externalCompany": "Kohdistamaton", "sapReports.reportFailed": "Raportin luonti epäonnistui.", + "sapReports.explicitEmpty": "Tyhjä arvo", "workTable.search.budgetTitle": "Talousarvio", "workTable.search.actualTitle": "Toteuma", "workTable.startDate": "Hakuaikavälin alku", diff --git a/shared/src/schema/code.ts b/shared/src/schema/code.ts index 2e6bef1e..9e9c264e 100644 --- a/shared/src/schema/code.ts +++ b/shared/src/schema/code.ts @@ -2,6 +2,10 @@ import { z } from 'zod'; import { Language, languages } from '../language'; +export const EXPLICIT_EMPTY = '0'; + +const emptyValueSchema = z.literal(EXPLICIT_EMPTY); + const codeIdRegex = /^\d{0,4}$/; export const codeId = z.string().regex(codeIdRegex); @@ -48,4 +52,5 @@ export type CodeId = z.infer; export const codeSearchSchema = z.object({ codeListId: codeListIdSchema, + allowEmptySelection: z.boolean().optional(), });