From 8087b66e53a2965c760fca059ea4423648248bab Mon Sep 17 00:00:00 2001 From: Casper Iversen <53900565+casperiv0@users.noreply.github.com> Date: Sat, 21 Oct 2023 16:36:58 +0200 Subject: [PATCH] feat: deleteable penal codes (#1854) ## Bug - [ ] Related issues linked using `closes: #number` ## Feature - [x] Related issues linked using closes # - [x] There is only 1 db migration with no breaking changes. ## Translations - [ ] `.json` files are formatted: `pnpm format` - [ ] Translations are correct - [ ] New translation? It's been added to `i18n.config.mjs` --- .../migration.sql | 8 ++++++ apps/api/prisma/schema.prisma | 6 ++--- apps/client/locales/en/leo.json | 3 ++- .../request-expungement-modal.tsx | 2 +- .../select/MultiValueContainerPenalCode.tsx | 4 ++- .../src/components/leo/ViolationsColumn.tsx | 26 +++++++++++++----- .../manage-record/manage-record-modal.tsx | 4 ++- .../manage-record/penal-codes-table.tsx | 4 ++- .../modals/manage-record/table-item-form.tsx | 27 ++++++++++++------- packages/types/src/api/admin.ts | 12 +++++---- packages/types/src/index.ts | 2 +- 11 files changed, 68 insertions(+), 30 deletions(-) create mode 100644 apps/api/prisma/migrations/20231021140807_deletable_penal_codes/migration.sql diff --git a/apps/api/prisma/migrations/20231021140807_deletable_penal_codes/migration.sql b/apps/api/prisma/migrations/20231021140807_deletable_penal_codes/migration.sql new file mode 100644 index 000000000..ea610c048 --- /dev/null +++ b/apps/api/prisma/migrations/20231021140807_deletable_penal_codes/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "Violation" DROP CONSTRAINT "Violation_penalCodeId_fkey"; + +-- AlterTable +ALTER TABLE "Violation" ALTER COLUMN "penalCodeId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "Violation" ADD CONSTRAINT "Violation_penalCodeId_fkey" FOREIGN KEY ("penalCodeId") REFERENCES "PenalCode"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index add27cff9..1bf6cee7c 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -709,13 +709,13 @@ model WarningNotApplicable { } model Violation { - id String @id @default(uuid()) + id String @id @default(uuid()) fine Int? jailTime Int? bail Int? communityService String? - penalCode PenalCode @relation(fields: [penalCodeId], references: [id]) - penalCodeId String + penalCode PenalCode? @relation(fields: [penalCodeId], references: [id], onDelete: SetNull) + penalCodeId String? records Record[] counts Int? } diff --git a/apps/client/locales/en/leo.json b/apps/client/locales/en/leo.json index 5ef9306af..01bd4bffb 100644 --- a/apps/client/locales/en/leo.json +++ b/apps/client/locales/en/leo.json @@ -386,7 +386,8 @@ "all": "All", "ungrouped": "Ungrouped", "counts": "Counts", - "stats": "Stats" + "stats": "Stats", + "deletedPenalCode": "Deleted Penal Code" }, "Bolos": { "activeBolos": "Active Bolos", diff --git a/apps/client/src/components/courthouse/expungement-requests/request-expungement-modal.tsx b/apps/client/src/components/courthouse/expungement-requests/request-expungement-modal.tsx index d4ae8b1a3..4e486ab7b 100644 --- a/apps/client/src/components/courthouse/expungement-requests/request-expungement-modal.tsx +++ b/apps/client/src/components/courthouse/expungement-requests/request-expungement-modal.tsx @@ -234,6 +234,6 @@ function ResultsForm({ result, onSuccess, handleClose }: ResultProps) { export function getTitles( record: GetManageExpungementRequests["pendingExpungementRequests"][number]["records"][number], ) { - const titles = record.violations.map((v) => v.penalCode.title); + const titles = record.violations.map((v) => v.penalCode?.title).filter(Boolean) as string[]; return titles.join(", "); } diff --git a/apps/client/src/components/form/select/MultiValueContainerPenalCode.tsx b/apps/client/src/components/form/select/MultiValueContainerPenalCode.tsx index 034020687..8d93501a7 100644 --- a/apps/client/src/components/form/select/MultiValueContainerPenalCode.tsx +++ b/apps/client/src/components/form/select/MultiValueContainerPenalCode.tsx @@ -2,9 +2,11 @@ import { components, type MultiValueGenericProps } from "react-select"; import type { PenalCode } from "@snailycad/types"; import { dataToSlate, Editor } from "components/editor/editor"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@snailycad/ui"; +import { useTranslations } from "use-intl"; export function MultiValueContainerPenalCode(props: MultiValueGenericProps) { const penalCode = props.data.value as PenalCode; + const t = useTranslations("Leo"); return ( @@ -15,7 +17,7 @@ export function MultiValueContainerPenalCode(props: MultiValueGenericProps) -

{penalCode.title}

+

{penalCode.title || t("deletedPenalCode")}

diff --git a/apps/client/src/components/leo/ViolationsColumn.tsx b/apps/client/src/components/leo/ViolationsColumn.tsx index 4b8c683ee..0a6e7f665 100644 --- a/apps/client/src/components/leo/ViolationsColumn.tsx +++ b/apps/client/src/components/leo/ViolationsColumn.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import type { Record } from "@snailycad/types"; +import type { Record, Violation } from "@snailycad/types"; import { dataToSlate, Editor } from "components/editor/editor"; import { useValues } from "context/ValuesContext"; import { useTranslations } from "next-intl"; @@ -12,12 +12,27 @@ interface Props { export function ViolationsColumn({ violations }: Props) { const { penalCode } = useValues(); const common = useTranslations("Common"); + const t = useTranslations("Leo"); const getPenalCode = React.useCallback( - (penalCodeId: string) => { - return penalCode.values.find((p) => p.id === penalCodeId) ?? null; + (violation: Violation | { penalCodeId: string | null }) => { + if (!violation.penalCodeId) { + return { title: t("deletedPenalCode"), description: common("none") }; + } + + const isViolation = "penalCode" in violation; + if (isViolation) { + return violation.penalCode; + } + + const isString = "penalCodeId" in violation; + if (isString) { + return penalCode.values.find((p) => p.id === violation.penalCodeId) ?? null; + } + + return null; }, - [penalCode.values], + [penalCode.values, common, t], ); if (violations.length <= 0) { @@ -29,9 +44,8 @@ export function ViolationsColumn({ violations }: Props) { {violations.map((violation, idx) => { const comma = idx !== violations.length - 1 ? ", " : ""; const key = "id" in violation ? violation.id : idx; + const penalCode = getPenalCode(violation); - const penalCode = - "penalCode" in violation ? violation.penalCode : getPenalCode(violation.penalCodeId); if (!penalCode) return null; return ( diff --git a/apps/client/src/components/leo/modals/manage-record/manage-record-modal.tsx b/apps/client/src/components/leo/modals/manage-record/manage-record-modal.tsx index 29ac9b19f..d907451de 100644 --- a/apps/client/src/components/leo/modals/manage-record/manage-record-modal.tsx +++ b/apps/client/src/components/leo/modals/manage-record/manage-record-modal.tsx @@ -75,6 +75,7 @@ interface Props { } interface CreateInitialRecordValuesOptions { + t: ReturnType>; type: RecordType; record?: Record | null; penalCodes: PenalCode[]; @@ -98,7 +99,7 @@ export function createInitialRecordValues(options: CreateInitialRecordValuesOpti violations: (options.record?.violations ?? []).map((v) => { return { - label: v.penalCode.title, + label: v.penalCode?.title ?? options.t("deletedPenalCode"), value: { ...v.penalCode, fine: { enabled: Boolean(v.fine), value: v.fine }, @@ -264,6 +265,7 @@ export function ManageRecordModal(props: Props) { const validate = handleValidate(schema); const INITIAL_VALUES = createInitialRecordValues({ + t, type: props.type, record: props.record, penalCodes, diff --git a/apps/client/src/components/leo/modals/manage-record/penal-codes-table.tsx b/apps/client/src/components/leo/modals/manage-record/penal-codes-table.tsx index 1541599e4..fef5ae4b2 100644 --- a/apps/client/src/components/leo/modals/manage-record/penal-codes-table.tsx +++ b/apps/client/src/components/leo/modals/manage-record/penal-codes-table.tsx @@ -24,6 +24,8 @@ export function PenalCodesTable({ isReadOnly, penalCodes, isWithinCardOrModal = let sum = 0; for (const violation of values.violations) { + // skip deleted penal code + if (!violation.value.id) continue; const counts = parseInt(String(violation.value.counts.value)) || 1; if (violation.value[type].value) { @@ -53,7 +55,7 @@ export function PenalCodesTable({ isReadOnly, penalCodes, isWithinCardOrModal = tableState={tableState} data={penalCodes.map((penalCode) => ({ id: penalCode.id, - title: penalCode.title, + title: penalCode.title || t("deletedPenalCode"), data: , }))} columns={[ diff --git a/apps/client/src/components/leo/modals/manage-record/table-item-form.tsx b/apps/client/src/components/leo/modals/manage-record/table-item-form.tsx index b89b4a6f3..6eda865a9 100644 --- a/apps/client/src/components/leo/modals/manage-record/table-item-form.tsx +++ b/apps/client/src/components/leo/modals/manage-record/table-item-form.tsx @@ -81,6 +81,7 @@ interface HandleValueChangeOptions { export function TableItemForm({ penalCode, isReadOnly }: Props) { const t = useTranslations("Leo"); const { LEO_BAIL } = useFeatureEnabled(); + const isDeleted = !penalCode.id; const minFine = getPenalCodeMinFines(penalCode); const maxFine = getPenalCodeMaxFines(penalCode); @@ -88,11 +89,12 @@ export function TableItemForm({ penalCode, isReadOnly }: Props) { const [minJailTime, maxJailTime] = penalCode.warningNotApplicable?.prisonTerm ?? []; const [minBail, maxBail] = penalCode.warningNotApplicable?.bail ?? []; - const jailTimeDisabled = isReadOnly || !penalCode.warningNotApplicable?.prisonTerm.length; - const bailDisabled = isReadOnly || !penalCode.warningNotApplicable?.bail.length; + const jailTimeDisabled = + isDeleted || isReadOnly || !penalCode.warningNotApplicable?.prisonTerm.length; + const bailDisabled = isDeleted || isReadOnly || !penalCode.warningNotApplicable?.bail.length; const warningNotApplicableDisabled = - isReadOnly || !penalCode.warningNotApplicableId || jailTimeDisabled; - const finesDisabled = isReadOnly || !hasFines(penalCode); + isDeleted || isReadOnly || !penalCode.warningNotApplicableId || jailTimeDisabled; + const finesDisabled = isDeleted || isReadOnly || !hasFines(penalCode); const { setFieldValue, values, errors } = useFormikContext>(); @@ -102,7 +104,7 @@ export function TableItemForm({ penalCode, isReadOnly }: Props) { >; const current = values.violations.find( - (v: SelectValue) => v.value?.id === penalCode.id, + (v: SelectValue>) => v.value?.id === penalCode.id, ); const currentValue = parseCurrentValue(current?.value ?? null); @@ -166,7 +168,7 @@ export function TableItemForm({ penalCode, isReadOnly }: Props) { name="fine.counts" onChange={(event) => handleValueChange({ fieldName: "counts", event })} type="number" - disabled={isReadOnly} + disabled={isDeleted || isReadOnly} className="max-w-[125px] min-w-[125px] py-0.5" value={transformField(currentValue.counts.value, "1")} /> @@ -199,7 +201,7 @@ export function TableItemForm({ penalCode, isReadOnly }: Props) { min={minFine} max={maxFine} type="number" - disabled={isReadOnly || !currentValue.fine.enabled} + disabled={isDeleted || isReadOnly || !currentValue.fine.enabled} className="max-w-[125px] min-w-[125px] ml-5 py-0.5" value={ !isNaN(parseFloat(String(currentValue.fine.value))) ? currentValue.fine.value : "" @@ -210,6 +212,7 @@ export function TableItemForm({ penalCode, isReadOnly }: Props) {
@@ -222,7 +225,7 @@ export function TableItemForm({ penalCode, isReadOnly }: Props) {