pembatasan akses delete dari pengelola rumah ibadah dan fitur upload bukti penyerahan bantuan

This commit is contained in:
2026-06-03 10:50:30 +07:00
parent 8393e05f96
commit 63c5d0a11f
5 changed files with 68 additions and 21 deletions
+2
View File
@@ -59,6 +59,7 @@ export const distributionRouter = createRouter({
quantity: z.number().min(1).default(1), quantity: z.number().min(1).default(1),
unit: z.string().default("package"), unit: z.string().default("package"),
notes: z.string().optional(), notes: z.string().optional(),
handoverPhoto: z.string().min(1),
}) })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
@@ -77,6 +78,7 @@ export const distributionRouter = createRouter({
amount: input.amount.toString(), amount: input.amount.toString(),
distributedBy: input.distributedBy ?? null, distributedBy: input.distributedBy ?? null,
notes: input.notes ?? null, notes: input.notes ?? null,
handoverPhoto: input.handoverPhoto,
}); });
if (created) { if (created) {
+4 -7
View File
@@ -250,13 +250,10 @@ export const recipientRouter = createRouter({
} }
if (ctx.user.role === "manager") { if (ctx.user.role === "manager") {
const placeIds = await findPlaceIdsByManagerId(ctx.user.id); throw new TRPCError({
if (!placeIds.includes(target.placeOfWorshipId)) { code: "FORBIDDEN",
throw new TRPCError({ message: "Pengelola rumah ibadah tidak berwenang menghapus data penerima.",
code: "FORBIDDEN", });
message: "Anda tidak berwenang menghapus data penerima ini.",
});
}
} }
await deleteRecipient(input.id); await deleteRecipient(input.id);
+1
View File
@@ -108,6 +108,7 @@ export const distributions = mysqlTable(
quantity: int("quantity").default(1), quantity: int("quantity").default(1),
unit: varchar("unit", { length: 50 }).default("package"), unit: varchar("unit", { length: 50 }).default("package"),
distributionDate: timestamp("distribution_date").defaultNow().notNull(), distributionDate: timestamp("distribution_date").defaultNow().notNull(),
handoverPhoto: longtext("handover_photo"),
notes: text("notes"), notes: text("notes"),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
}, },
+47 -2
View File
@@ -2,6 +2,8 @@ import { useMemo, useState } from "react";
import { trpc } from "@/providers/trpc"; import { trpc } from "@/providers/trpc";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { LocationPicker } from "@/components/LocationPicker"; import { LocationPicker } from "@/components/LocationPicker";
import { DocumentUploadField } from "@/components/DocumentUploadField";
import { toast } from "sonner";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -80,6 +82,7 @@ export default function PlacesOfWorshipPage() {
quantity: 1, quantity: 1,
unit: "paket", unit: "paket",
notes: "", notes: "",
handoverPhoto: "",
}); });
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
@@ -142,6 +145,7 @@ export default function PlacesOfWorshipPage() {
quantity: 1, quantity: 1,
unit: "paket", unit: "paket",
notes: "", notes: "",
handoverPhoto: "",
}); });
}, },
}); });
@@ -232,6 +236,10 @@ export default function PlacesOfWorshipPage() {
const handleAidSubmit = (e: React.FormEvent) => { const handleAidSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!selectedPlace || !aidForm.recipientId) return; if (!selectedPlace || !aidForm.recipientId) return;
if (!aidForm.handoverPhoto) {
toast.error("Foto penyerahan wajib diunggah.");
return;
}
createAidMutation.mutate({ createAidMutation.mutate({
placeOfWorshipId: selectedPlace, placeOfWorshipId: selectedPlace,
recipientId: aidForm.recipientId, recipientId: aidForm.recipientId,
@@ -240,6 +248,7 @@ export default function PlacesOfWorshipPage() {
quantity: aidForm.quantity, quantity: aidForm.quantity,
unit: aidForm.unit, unit: aidForm.unit,
notes: aidForm.notes || undefined, notes: aidForm.notes || undefined,
handoverPhoto: aidForm.handoverPhoto,
}); });
}; };
@@ -507,7 +516,7 @@ export default function PlacesOfWorshipPage() {
/> />
</div> </div>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">Catatan</Label> <Label className="text-xs">Catatan</Label>
<Input <Input
value={aidForm.notes} value={aidForm.notes}
@@ -515,7 +524,17 @@ export default function PlacesOfWorshipPage() {
className="h-9" className="h-9"
/> />
</div> </div>
<Button type="submit" className="w-full" disabled={createAidMutation.isPending || !aidForm.recipientId}> <div className="space-y-1.5">
<DocumentUploadField
label="Foto Penyerahan Bantuan"
value={aidForm.handoverPhoto}
onChange={(value) => setAidForm((current) => ({ ...current, handoverPhoto: value ?? "" }))}
required={true}
helperText="Wajib menampakkan wajah penerima bantuan saat penyerahan"
accept="image/*"
/>
</div>
<Button type="submit" className="w-full" disabled={createAidMutation.isPending || !aidForm.recipientId || !aidForm.handoverPhoto}>
{createAidMutation.isPending ? "Menyimpan bantuan..." : "Simpan Bantuan"} {createAidMutation.isPending ? "Menyimpan bantuan..." : "Simpan Bantuan"}
</Button> </Button>
</form> </form>
@@ -536,6 +555,7 @@ export default function PlacesOfWorshipPage() {
<th className="text-left py-3 px-3 text-xs font-medium text-muted-foreground">Jenis</th> <th className="text-left py-3 px-3 text-xs font-medium text-muted-foreground">Jenis</th>
<th className="text-left py-3 px-3 text-xs font-medium text-muted-foreground">Jumlah</th> <th className="text-left py-3 px-3 text-xs font-medium text-muted-foreground">Jumlah</th>
<th className="text-left py-3 px-3 text-xs font-medium text-muted-foreground">Catatan</th> <th className="text-left py-3 px-3 text-xs font-medium text-muted-foreground">Catatan</th>
<th className="text-left py-3 px-3 text-xs font-medium text-muted-foreground">Foto Bukti</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -554,6 +574,31 @@ export default function PlacesOfWorshipPage() {
<td className="py-3 px-3 text-xs">{distribution.aidType}</td> <td className="py-3 px-3 text-xs">{distribution.aidType}</td>
<td className="py-3 px-3 text-xs">{formatRupiah(Number.parseFloat(String(distribution.amount ?? 0)))}</td> <td className="py-3 px-3 text-xs">{formatRupiah(Number.parseFloat(String(distribution.amount ?? 0)))}</td>
<td className="py-3 px-3 text-xs text-muted-foreground">{distribution.notes ?? "-"}</td> <td className="py-3 px-3 text-xs text-muted-foreground">{distribution.notes ?? "-"}</td>
<td className="py-3 px-3 text-xs">
{distribution.handoverPhoto ? (
<Dialog>
<DialogTrigger asChild>
<img
src={distribution.handoverPhoto}
alt="Bukti Penyerahan"
className="h-8 w-12 object-cover rounded cursor-pointer border hover:opacity-80 transition-opacity"
/>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Foto Penyerahan Bantuan</DialogTitle>
</DialogHeader>
<img
src={distribution.handoverPhoto}
alt="Bukti Penyerahan"
className="w-full max-h-[70vh] object-contain rounded-lg border bg-white"
/>
</DialogContent>
</Dialog>
) : (
<span className="text-muted-foreground">-</span>
)}
</td>
</tr> </tr>
); );
})} })}
+14 -12
View File
@@ -576,18 +576,20 @@ export default function RecipientsPage() {
> >
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-3.5 w-3.5" />
</Button> </Button>
<Button {isAdmin && (
variant="ghost" <Button
size="icon" variant="ghost"
className="h-7 w-7 text-destructive" size="icon"
onClick={() => { className="h-7 w-7 text-destructive"
if (confirm("Hapus penerima ini?")) { onClick={() => {
deleteMutation.mutate({ id: r.id }); if (confirm("Hapus penerima ini?")) {
} deleteMutation.mutate({ id: r.id });
}} }
> }}
<Trash2 className="h-3.5 w-3.5" /> >
</Button> <Trash2 className="h-3.5 w-3.5" />
</Button>
)}
</> </>
)} )}
</div> </div>