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),
|
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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user