From 4cfe6d88d5ad1429c884ac53d0b72a4e6ba6f8d7 Mon Sep 17 00:00:00 2001 From: Mhanif26 Date: Sat, 16 May 2026 22:10:55 +0700 Subject: [PATCH] tambah fitur upload data pendukung --- api/recipientRouter.ts | 9 ++ db/schema.ts | 3 + src/components/DocumentUploadField.tsx | 132 +++++++++++++++++++++++++ src/pages/RecipientsPage.tsx | 81 ++++++++++++++- src/pages/VerificationPage.tsx | 11 +++ 5 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 src/components/DocumentUploadField.tsx diff --git a/api/recipientRouter.ts b/api/recipientRouter.ts index 4b958a5..6c225f6 100644 --- a/api/recipientRouter.ts +++ b/api/recipientRouter.ts @@ -60,6 +60,9 @@ export const recipientRouter = createRouter({ placeOfWorshipId: z.number().min(1), registeredBy: z.number().optional(), notes: z.string().optional(), + ktpDocument: z.string().min(1), + kkDocument: z.string().min(1), + sktmDocument: z.string().optional(), }) ) .mutation(async ({ input }) => { @@ -73,6 +76,9 @@ export const recipientRouter = createRouter({ phone: input.phone ?? null, registeredBy: input.registeredBy ?? null, notes: input.notes ?? null, + ktpDocument: input.ktpDocument, + kkDocument: input.kkDocument, + sktmDocument: input.sktmDocument ?? null, status: "pending", }); @@ -105,6 +111,9 @@ export const recipientRouter = createRouter({ incomePerMonth: z.number().min(0).optional(), placeOfWorshipId: z.number().optional(), notes: z.string().optional(), + ktpDocument: z.string().optional(), + kkDocument: z.string().optional(), + sktmDocument: z.string().optional(), }) ) .mutation(async ({ input }) => { diff --git a/db/schema.ts b/db/schema.ts index b38c39b..9fb7060 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -78,6 +78,9 @@ export const recipients = mysqlTable( verifiedBy: bigint("verified_by", { mode: "number", unsigned: true }), verifiedAt: timestamp("verified_at"), notes: text("notes"), + ktpDocument: text("ktp_document"), + kkDocument: text("kk_document"), + sktmDocument: text("sktm_document"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull().$onUpdate(() => new Date()), }, diff --git a/src/components/DocumentUploadField.tsx b/src/components/DocumentUploadField.tsx new file mode 100644 index 0000000..4d15cd8 --- /dev/null +++ b/src/components/DocumentUploadField.tsx @@ -0,0 +1,132 @@ +import { useId, useMemo, type ChangeEvent } from "react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { FileUp, FileText, Image as ImageIcon, X } from "lucide-react"; + +type DocumentUploadFieldProps = { + label: string; + value?: string | null; + onChange?: (value: string | null) => void; + required?: boolean; + helperText?: string; + accept?: string; + className?: string; +}; + +function readFileAsDataUrl(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result ?? "")); + reader.onerror = () => reject(reader.error ?? new Error("Gagal membaca file")); + reader.readAsDataURL(file); + }); +} + +function getPreviewKind(value?: string | null) { + if (!value) return "none"; + if (value.startsWith("data:application/pdf")) return "pdf"; + if (value.startsWith("data:image/")) return "image"; + return "file"; +} + +export function DocumentUploadField({ + label, + value, + onChange, + required = false, + helperText, + accept = "image/*,application/pdf", + className = "", +}: DocumentUploadFieldProps) { + const inputId = useId(); + const previewKind = useMemo(() => getPreviewKind(value), [value]); + + const handleSelectFile = async (event: ChangeEvent) => { + const file = event.target.files?.[0]; + event.target.value = ""; + if (!file || !onChange) return; + const dataUrl = await readFileAsDataUrl(file); + onChange(dataUrl); + }; + + const canEdit = Boolean(onChange); + + return ( +
+
+ + {canEdit && value ? ( + + ) : null} +
+ {helperText ?

{helperText}

: null} + +