298 lines
10 KiB
TypeScript
298 lines
10 KiB
TypeScript
import type { PaginationState } from '@/components/data-table';
|
|
import { DataTable } from '@/components/data-table';
|
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
|
import type { FilterField } from '@/components/filter-dialog';
|
|
import { FilterDialog } from '@/components/filter-dialog';
|
|
import { PageHeader } from '@/components/page-header';
|
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Label } from '@/components/ui/label';
|
|
import { usePermissions } from '@/hooks/use-permissions';
|
|
import { useServerTable } from '@/hooks/use-server-table';
|
|
import { richTextContentClass } from '@/lib/rich-text-content';
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
create,
|
|
destroy,
|
|
edit,
|
|
index as feedbackIndex,
|
|
update_status,
|
|
} from '@/routes/admin/feedback';
|
|
import type { Feedback } from '@/types/feedback';
|
|
import { FeedbackStatusLabels, FeedbackTypeLabels } from '@/types/feedback';
|
|
import { Head, Link, router } from '@inertiajs/react';
|
|
import { format } from 'date-fns';
|
|
import { Info, Plus } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { createFeedbackColumns, FeedbackStatusVariants } from './columns';
|
|
|
|
type FeedbackTypeOption = { value: string; label: string };
|
|
|
|
type Props = {
|
|
feedbacks: {
|
|
data: Feedback[];
|
|
current_page: number;
|
|
last_page: number;
|
|
per_page: number;
|
|
total: number;
|
|
};
|
|
types: FeedbackTypeOption[];
|
|
statuses: FeedbackTypeOption[];
|
|
filters: {
|
|
type?: string;
|
|
status?: string;
|
|
};
|
|
};
|
|
|
|
export default function FeedbackIndex({
|
|
feedbacks,
|
|
types,
|
|
statuses,
|
|
filters,
|
|
}: Props) {
|
|
const [deleting, setDeleting] = useState<Feedback | null>(null);
|
|
const [viewing, setViewing] = useState<Feedback | null>(null);
|
|
const { hasPermission } = usePermissions();
|
|
const canCreate = hasPermission('create-feedback');
|
|
const canUpdate = hasPermission('update-feedback');
|
|
const canDelete = hasPermission('delete-feedback');
|
|
const canUpdateStatus = hasPermission('update-feedback-status');
|
|
|
|
const filterFields: FilterField[] = [
|
|
{
|
|
key: 'type',
|
|
label: 'Jenis',
|
|
options: types,
|
|
},
|
|
{
|
|
key: 'status',
|
|
label: 'Status',
|
|
options: statuses,
|
|
},
|
|
];
|
|
|
|
const pagination: PaginationState = {
|
|
current_page: feedbacks.current_page,
|
|
last_page: feedbacks.last_page,
|
|
per_page: feedbacks.per_page,
|
|
total: feedbacks.total,
|
|
};
|
|
|
|
const {
|
|
search,
|
|
handlePageChange,
|
|
handlePerPageChange,
|
|
handleSearchChange,
|
|
applyFilters,
|
|
} = useServerTable({
|
|
route: () => feedbackIndex.url(),
|
|
pagination,
|
|
filters,
|
|
});
|
|
|
|
function handleDelete() {
|
|
if (!deleting) {
|
|
return;
|
|
}
|
|
|
|
router.delete(destroy(deleting.id), {
|
|
onSuccess: () => setDeleting(null),
|
|
});
|
|
}
|
|
|
|
function handleStatusChange(feedback: Feedback, status: string) {
|
|
router.patch(
|
|
update_status.url(feedback.id),
|
|
{ status },
|
|
{ preserveScroll: true },
|
|
);
|
|
}
|
|
|
|
const columns = createFeedbackColumns({
|
|
handleView: (feedback) => setViewing(feedback),
|
|
handleEdit: (feedback) => router.get(edit.url(feedback.id)),
|
|
handleDeleteClick: (feedback) => setDeleting(feedback),
|
|
handleStatusChange,
|
|
statuses,
|
|
canUpdateStatus,
|
|
canUpdate,
|
|
canDelete,
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<Head title="Kritik dan Saran" />
|
|
|
|
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
|
<PageHeader
|
|
title="Kritik dan Saran"
|
|
actions={
|
|
canCreate && (
|
|
<Button asChild>
|
|
<Link href={create.url()}>
|
|
<Plus className="h-4 w-4" />
|
|
Tambah
|
|
</Link>
|
|
</Button>
|
|
)
|
|
}
|
|
/>
|
|
|
|
<ViewDetailDialog
|
|
open={viewing !== null}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setViewing(null);
|
|
}
|
|
}}
|
|
feedback={viewing}
|
|
/>
|
|
|
|
{canUpdateStatus && (
|
|
<Alert>
|
|
<Info />
|
|
<AlertTitle>Ubah status Kritik dan Saran</AlertTitle>
|
|
<AlertDescription>
|
|
Klik badge Status pada tabel untuk mengubah status
|
|
Kritik dan Saran secara langsung.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<DataTable
|
|
columns={columns}
|
|
data={feedbacks.data}
|
|
pagination={pagination}
|
|
onPageChange={handlePageChange}
|
|
onPerPageChange={handlePerPageChange}
|
|
onSearchChange={handleSearchChange}
|
|
searchValue={search}
|
|
searchKey="subject"
|
|
toolbar={
|
|
<FilterDialog
|
|
fields={filterFields}
|
|
activeFilters={filters}
|
|
onApply={applyFilters}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<DeleteConfirmDialog
|
|
target={deleting}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setDeleting(null);
|
|
}
|
|
}}
|
|
title="Hapus Masukan"
|
|
description={(feedback) =>
|
|
`Apakah Anda yakin ingin menghapus "${feedback.subject}"? Tindakan ini tidak dapat dibatalkan.`
|
|
}
|
|
onConfirm={handleDelete}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ViewDetailDialog({
|
|
open,
|
|
onOpenChange,
|
|
feedback,
|
|
}: {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
feedback: Feedback | null;
|
|
}) {
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-3xl">
|
|
<DialogHeader className="shrink-0">
|
|
<DialogTitle>Detail Kritik dan Saran</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{feedback && (
|
|
<div className="grid min-h-0 flex-1 gap-4 overflow-x-hidden overflow-y-auto">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Badge variant="outline">
|
|
{FeedbackTypeLabels[feedback.type]}
|
|
</Badge>
|
|
<Badge
|
|
variant={
|
|
FeedbackStatusVariants[feedback.status]
|
|
}
|
|
>
|
|
{FeedbackStatusLabels[feedback.status]}
|
|
</Badge>
|
|
<span className="text-xs text-muted-foreground">
|
|
Dikirim{' '}
|
|
{format(
|
|
new Date(feedback.created_at),
|
|
'd MMM yyyy, HH:mm',
|
|
)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid gap-1">
|
|
<Label className="text-muted-foreground">
|
|
Subjek
|
|
</Label>
|
|
<p className="font-medium">{feedback.subject}</p>
|
|
</div>
|
|
|
|
<div className="grid gap-1">
|
|
<Label className="text-muted-foreground">
|
|
Dibuat Oleh
|
|
</Label>
|
|
<p>{feedback.user?.profile?.full_name ?? '-'}</p>
|
|
</div>
|
|
|
|
<div className="grid gap-1">
|
|
<Label className="text-muted-foreground">
|
|
Ditindaklanjuti Oleh
|
|
</Label>
|
|
<p>
|
|
{feedback.handler?.profile?.full_name ??
|
|
'Belum ditindaklanjuti'}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-1">
|
|
<Label className="text-muted-foreground">
|
|
Pesan
|
|
</Label>
|
|
<div
|
|
className={cn(
|
|
'rounded-md border p-3 text-sm',
|
|
richTextContentClass,
|
|
)}
|
|
dangerouslySetInnerHTML={{
|
|
__html: feedback.message,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{feedback.admin_notes && (
|
|
<div className="grid gap-1">
|
|
<Label className="text-muted-foreground">
|
|
Catatan Admin
|
|
</Label>
|
|
<p className="rounded-md border bg-muted/50 p-3 text-sm whitespace-pre-line">
|
|
{feedback.admin_notes}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|