pembatasan akses delete dari pengelola rumah ibadah dan fitur upload bukti penyerahan bantuan
This commit is contained in:
@@ -59,6 +59,7 @@ export const distributionRouter = createRouter({
|
||||
quantity: z.number().min(1).default(1),
|
||||
unit: z.string().default("package"),
|
||||
notes: z.string().optional(),
|
||||
handoverPhoto: z.string().min(1),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -77,6 +78,7 @@ export const distributionRouter = createRouter({
|
||||
amount: input.amount.toString(),
|
||||
distributedBy: input.distributedBy ?? null,
|
||||
notes: input.notes ?? null,
|
||||
handoverPhoto: input.handoverPhoto,
|
||||
});
|
||||
|
||||
if (created) {
|
||||
|
||||
@@ -250,13 +250,10 @@ export const recipientRouter = createRouter({
|
||||
}
|
||||
|
||||
if (ctx.user.role === "manager") {
|
||||
const placeIds = await findPlaceIdsByManagerId(ctx.user.id);
|
||||
if (!placeIds.includes(target.placeOfWorshipId)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Anda tidak berwenang menghapus data penerima ini.",
|
||||
});
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Pengelola rumah ibadah tidak berwenang menghapus data penerima.",
|
||||
});
|
||||
}
|
||||
|
||||
await deleteRecipient(input.id);
|
||||
|
||||
@@ -108,6 +108,7 @@ export const distributions = mysqlTable(
|
||||
quantity: int("quantity").default(1),
|
||||
unit: varchar("unit", { length: 50 }).default("package"),
|
||||
distributionDate: timestamp("distribution_date").defaultNow().notNull(),
|
||||
handoverPhoto: longtext("handover_photo"),
|
||||
notes: text("notes"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
},
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useMemo, useState } from "react";
|
||||
import { trpc } from "@/providers/trpc";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { LocationPicker } from "@/components/LocationPicker";
|
||||
import { DocumentUploadField } from "@/components/DocumentUploadField";
|
||||
import { toast } from "sonner";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -80,6 +82,7 @@ export default function PlacesOfWorshipPage() {
|
||||
quantity: 1,
|
||||
unit: "paket",
|
||||
notes: "",
|
||||
handoverPhoto: "",
|
||||
});
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
@@ -142,6 +145,7 @@ export default function PlacesOfWorshipPage() {
|
||||
quantity: 1,
|
||||
unit: "paket",
|
||||
notes: "",
|
||||
handoverPhoto: "",
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -232,6 +236,10 @@ export default function PlacesOfWorshipPage() {
|
||||
const handleAidSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!selectedPlace || !aidForm.recipientId) return;
|
||||
if (!aidForm.handoverPhoto) {
|
||||
toast.error("Foto penyerahan wajib diunggah.");
|
||||
return;
|
||||
}
|
||||
createAidMutation.mutate({
|
||||
placeOfWorshipId: selectedPlace,
|
||||
recipientId: aidForm.recipientId,
|
||||
@@ -240,6 +248,7 @@ export default function PlacesOfWorshipPage() {
|
||||
quantity: aidForm.quantity,
|
||||
unit: aidForm.unit,
|
||||
notes: aidForm.notes || undefined,
|
||||
handoverPhoto: aidForm.handoverPhoto,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -507,7 +516,7 @@ export default function PlacesOfWorshipPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Catatan</Label>
|
||||
<Input
|
||||
value={aidForm.notes}
|
||||
@@ -515,7 +524,17 @@ export default function PlacesOfWorshipPage() {
|
||||
className="h-9"
|
||||
/>
|
||||
</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"}
|
||||
</Button>
|
||||
</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">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">Foto Bukti</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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">{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">
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -576,18 +576,20 @@ export default function RecipientsPage() {
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive"
|
||||
onClick={() => {
|
||||
if (confirm("Hapus penerima ini?")) {
|
||||
deleteMutation.mutate({ id: r.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive"
|
||||
onClick={() => {
|
||||
if (confirm("Hapus penerima ini?")) {
|
||||
deleteMutation.mutate({ id: r.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user