Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: citizen records payments (Optional Feature) #1833

Merged
merged 5 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "Feature" ADD VALUE 'CITIZEN_RECORD_PAYMENTS';
1 change: 1 addition & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -1989,6 +1989,7 @@ enum Feature {
LEO_EDITABLE_CITIZEN_PROFILE // see #1698
ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER // see #1722
OPEN_LAW_BOOK
CITIZEN_RECORD_PAYMENTS
}

enum DashboardLayoutCardType {
Expand Down
50 changes: 50 additions & 0 deletions apps/api/src/controllers/record/records-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
Officer,
User,
Business,
PaymentStatus,
} from "@prisma/client";
import { validateSchema } from "lib/data/validate-schema";
import { combinedUnitProperties, leoProperties } from "utils/leo/includes";
Expand Down Expand Up @@ -52,6 +53,7 @@ import { Descendant, slateDataToString } from "@snailycad/utils/editor";
import puppeteer from "puppeteer";
import { AuditLogActionType, createAuditLogEntry } from "@snailycad/audit-logger/server";
import { captureException } from "@sentry/node";
import { shouldCheckCitizenUserId } from "~/lib/citizen/has-citizen-access";

export const assignedOfficersInclude = {
combinedUnit: { include: combinedUnitProperties },
Expand Down Expand Up @@ -389,6 +391,54 @@ export class RecordsController {
return recordItem;
}

@Post("/mark-as-paid/:id")
@Description("Allow a citizen to mark a record as paid")
@IsFeatureEnabled({
feature: Feature.CITIZEN_RECORD_PAYMENTS,
})
async markRecordAsPaid(
@Context("cad") cad: { features?: Record<Feature, boolean> },
@Context("user") user: User,
@PathParams("id") recordId: string,
): Promise<APITypes.PutRecordsByIdData> {
const checkCitizenUserId = shouldCheckCitizenUserId({ cad, user });

const citizen = await prisma.citizen.findFirst({
where: {
userId: checkCitizenUserId ? user.id : undefined,
Record: { some: { id: recordId } },
},
});

if (!citizen) {
throw new NotFound("citizenNotFound");
}

const record = await prisma.record.findFirst({
where: { id: recordId },
});

if (!record) {
throw new NotFound("recordNotFound");
}

const isEnabled = isFeatureEnabled({
feature: Feature.CITIZEN_RECORD_APPROVAL,
features: cad.features,
defaultReturn: false,
});

const updatedRecord = await prisma.record.update({
where: { id: recordId },
data: {
paymentStatus: PaymentStatus.PAID,
},
include: recordsInclude(isEnabled).include,
});

return updatedRecord;
}

@UseBefore(ActiveOfficer)
@Delete("/:id")
@Description("Delete a warrant, ticket, written warning or arrest report by its id")
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/middlewares/is-enabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const DEFAULT_DISABLED_FEATURES: Partial<
REQUIRED_CITIZEN_IMAGE: { isEnabled: false },
LEO_EDITABLE_CITIZEN_PROFILE: { isEnabled: false },
ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER: { isEnabled: false },
CITIZEN_RECORD_PAYMENTS: { isEnabled: false },
};

export type CadFeatures = Record<TypesFeature | DatabaseFeature, boolean> & {
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/migrations/set-default-cad-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const DEFAULT_DISABLED_FEATURES: Partial<Record<Feature, { isEnabled: boolean }>
REQUIRED_CITIZEN_IMAGE: { isEnabled: false },
LEO_EDITABLE_CITIZEN_PROFILE: { isEnabled: false },
ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER: { isEnabled: false },
CITIZEN_RECORD_PAYMENTS: { isEnabled: false },
};

/**
Expand Down
4 changes: 3 additions & 1 deletion apps/client/locales/en/cad-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,9 @@
"ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER": "Allow multiple units with the same callsign and department per user",
"ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER-description": "When enabled, officers and deputies can create multiple units with the same callsign and department.",
"OPEN_LAW_BOOK": "Open Law Book",
"OPEN_LAW_BOOK-description": "When enabled, this will allow every user to view the CAD's penal codes (Law Book)"
"OPEN_LAW_BOOK-description": "When enabled, this will allow every user to view the CAD's penal codes (Law Book)",
"CITIZEN_RECORD_PAYMENTS": "Citizen Record Payments",
"CITIZEN_RECORD_PAYMENTS-description": "When enabled, this will allow citizens to mark their own records as paid."
},
"Permissions": {
"defaultPermissions": "Default Permissions",
Expand Down
3 changes: 2 additions & 1 deletion apps/client/locales/en/citizen.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
"huntingLicenseCategory": "Hunting License Categories",
"huntingLicensePoints": "Hunting License Points",
"fishingLicensePoints": "Fishing License Points",
"otherLicenseCategory": "Other License Categories"
"otherLicenseCategory": "Other License Categories",
"markAsPaid": "Mark As Paid"
},
"Vehicles": {
"model": "Model",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import compareDesc from "date-fns/compareDesc";
import { useRouter } from "next/router";
import { Record, RecordType } from "@snailycad/types";
import { PaymentStatus, Record, RecordType } from "@snailycad/types";
import { useTranslations } from "use-intl";
import { Button, FullDate, Loader, Status, TabsContent } from "@snailycad/ui";
import { ModalIds } from "types/modal-ids";
Expand All @@ -14,10 +14,11 @@ import { Table, useTableState } from "components/shared/Table";
import { ManageRecordModal } from "../../manage-record/manage-record-modal";
import { Permissions, usePermission } from "hooks/usePermission";
import { ViolationsColumn } from "components/leo/ViolationsColumn";
import type { DeleteRecordsByIdData } from "@snailycad/types/api";
import type { DeleteRecordsByIdData, PutRecordsByIdData } from "@snailycad/types/api";
import { RecordsCaseNumberColumn } from "components/leo/records-case-number-column";
import { CallDescription } from "components/dispatch/active-calls/CallDescription";
import { getAPIUrl } from "@snailycad/utils/api-url";
import { useFeatureEnabled } from "hooks/useFeatureEnabled";

interface RecordsTabProps {
records: Record[];
Expand Down Expand Up @@ -95,7 +96,7 @@ export function RecordsTab({
{data.length <= 0 ? (
<p className="text-neutral-700 dark:text-gray-400 my-2">{noValuesText}</p>
) : (
<RecordsTable currentResult={currentResult} data={data} />
<RecordsTable onEdit={handleRecordUpdate} currentResult={currentResult} data={data} />
)}
</React.Fragment>
) : (
Expand Down Expand Up @@ -151,13 +152,15 @@ export function RecordsTable({
const modalState = useModal();
const t = useTranslations();
const router = useRouter();
const { execute, state } = useFetch();

const isCitizenCreation = router.pathname === "/citizen/create";
const isCitizen = router.pathname.startsWith("/citizen") && !isCitizenCreation;

const { generateCallsign } = useGenerateCallsign();
const tableState = useTableState();
const currency = common("currency");
const { CITIZEN_RECORD_PAYMENTS } = useFeatureEnabled();

const { hasPermissions } = usePermission();
const _hasDeletePermissions =
Expand Down Expand Up @@ -187,6 +190,17 @@ export function RecordsTable({
document.body.removeChild(anchor);
}

async function handleMarkAsPaid(record: Record) {
const { json } = await execute<PutRecordsByIdData>({
path: `/records/mark-as-paid/${record.id}`,
method: "POST",
});

if (json.id) {
onEdit?.(json);
}
}

async function handleExportClick(record: Record) {
setExportState("loading");

Expand Down Expand Up @@ -265,9 +279,27 @@ export function RecordsTable({
/>
),
createdAt: <FullDate>{record.createdAt}</FullDate>,
actions: isCitizen ? null : (
actions: (
<>
{isCitizenCreation ? null : (
{isCitizen && CITIZEN_RECORD_PAYMENTS ? (
record.paymentStatus === PaymentStatus.PAID ? (
"—"
) : (
<Button
variant="success"
type="button"
onPress={() => handleMarkAsPaid(record)}
size="xs"
className="inline-flex mr-2 items-center gap-2"
disabled={state === "loading"}
>
{state === "loading" ? <Loader className="w-3 h-3" /> : null}
{t("Citizen.markAsPaid")}
</Button>
)
) : null}

{isCitizen ? null : (
<>
<Button
type="button"
Expand All @@ -288,20 +320,20 @@ export function RecordsTable({
>
{common("edit")}
</Button>

{_hasDeletePermissions ? (
<Button
className="ml-2"
type="button"
onPress={() => handleDeleteClick(record)}
size="xs"
variant="danger"
>
{common("delete")}
</Button>
) : null}
</>
)}

{_hasDeletePermissions ? (
<Button
className="ml-2"
type="button"
onPress={() => handleDeleteClick(record)}
size="xs"
variant="danger"
>
{common("delete")}
</Button>
) : null}
</>
),
};
Expand All @@ -314,8 +346,8 @@ export function RecordsTable({
{ header: t("Leo.officer"), accessorKey: "officer" },
{ header: t("Leo.paymentStatus"), accessorKey: "paymentStatus" },
isCitizen ? { header: t("Leo.totalCost"), accessorKey: "totalCost" } : null,
isCitizenCreation ? null : { header: common("createdAt"), accessorKey: "createdAt" },
isCitizen ? null : { header: common("actions"), accessorKey: "actions" },
{ header: common("createdAt"), accessorKey: "createdAt" },
{ header: common("actions"), accessorKey: "actions" },
]}
/>
</div>
Expand Down
1 change: 1 addition & 0 deletions apps/client/src/hooks/useFeatureEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const DEFAULT_DISABLED_FEATURES = {
REQUIRED_CITIZEN_IMAGE: { isEnabled: false },
LEO_EDITABLE_CITIZEN_PROFILE: { isEnabled: false },
ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER: { isEnabled: false },
CITIZEN_RECORD_PAYMENTS: { isEnabled: false },
} satisfies Partial<Record<Feature, { isEnabled: boolean }>>;

export const DEFAULT_FEATURE_OPTIONS = {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const Feature = {
LEO_EDITABLE_CITIZEN_PROFILE: "LEO_EDITABLE_CITIZEN_PROFILE",
ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER: "ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER",
OPEN_LAW_BOOK: "OPEN_LAW_BOOK",
CITIZEN_RECORD_PAYMENTS: "CITIZEN_RECORD_PAYMENTS",
} as const;

export type Feature = (typeof Feature)[keyof typeof Feature];
Expand Down
4 changes: 0 additions & 4 deletions packages/utils/src/api-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
export function getAPIUrl(): string {
const envUrl = process.env.NEXT_PUBLIC_PROD_ORIGIN ?? "http://localhost:8080/v1";

if (process.env.NODE_ENV === "development") {
return "http://localhost:8080/v1";
}

if (envUrl.endsWith("/v1")) {
return envUrl;
}
Expand Down
Loading