Compare commits

...

3 Commits

5 changed files with 44 additions and 37 deletions

View File

@ -41,18 +41,6 @@ public function __construct()
$this->sanitizer = new HtmlSanitizer($config); $this->sanitizer = new HtmlSanitizer($config);
} }
public function paginated(User $user, int $perPage = 25, string $search = '', ?string $type = null, ?string $status = null): LengthAwarePaginator
{
return Feedback::query()
->where('user_id', $user->id)
->with('handler.profile')
->when($search, fn($q) => $q->where('subject', 'like', "%{$search}%"))
->when($type, fn($q) => $q->where('type', $type))
->when($status, fn($q) => $q->where('status', $status))
->latest()
->paginate($perPage);
}
public function pendingCount(User $user): int public function pendingCount(User $user): int
{ {
return Feedback::query() return Feedback::query()
@ -61,6 +49,18 @@ public function pendingCount(User $user): int
->count(); ->count();
} }
public function paginated(User $user, int $perPage = 25, string $search = '', ?string $type = null, ?string $status = null): LengthAwarePaginator
{
return Feedback::query()
->where('user_id', $user->id)
->with(['user.profile', 'handler.profile'])
->when($search, fn ($q) => $q->where('subject', 'like', "%{$search}%"))
->when($type, fn ($q) => $q->where('type', $type))
->when($status, fn ($q) => $q->where('status', $status))
->latest()
->paginate($perPage);
}
public function create(User $user, array $data): Feedback public function create(User $user, array $data): Feedback
{ {
return Feedback::create([ return Feedback::create([

View File

@ -9,6 +9,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { cn } from '@/lib/utils';
type FormDialogProps = { type FormDialogProps = {
open: boolean; open: boolean;
@ -44,23 +45,29 @@ export function FormDialog({
}: FormDialogProps) { }: FormDialogProps) {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={contentClassName}> <DialogContent
className={cn(
'flex max-h-[85vh] flex-col overflow-hidden',
contentClassName,
)}
>
<Form <Form
action={action} action={action}
resetOnSuccess={resetOnSuccess} resetOnSuccess={resetOnSuccess}
onSuccess={onSuccess} onSuccess={onSuccess}
className="flex min-h-0 flex-1 flex-col"
> >
{({ errors, processing }) => ( {({ errors, processing }) => (
<> <>
<DialogHeader> <DialogHeader className="shrink-0">
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid flex-1 gap-4 overflow-y-auto py-4">
{typeof children === 'function' {typeof children === 'function'
? children({ errors, processing }) ? children({ errors, processing })
: children} : children}
</div> </div>
<DialogFooter> <DialogFooter className="shrink-0">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"

View File

@ -31,13 +31,6 @@ export const FeedbackStatusVariants: Record<
resolved: 'default', resolved: 'default',
}; };
function stripHtml(html: string): string {
return html
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
export function createFeedbackColumns( export function createFeedbackColumns(
params: CreateColumnsParams, params: CreateColumnsParams,
): ColumnDef<Feedback>[] { ): ColumnDef<Feedback>[] {
@ -56,18 +49,6 @@ export function createFeedbackColumns(
{ {
accessorKey: 'subject', accessorKey: 'subject',
header: () => <span>Subjek</span>, header: () => <span>Subjek</span>,
cell: ({ row }) => {
const feedback = row.original;
return (
<div>
<p className="font-medium">{feedback.subject}</p>
<p className="line-clamp-1 text-xs text-muted-foreground">
{stripHtml(feedback.message)}
</p>
</div>
);
},
}, },
{ {
accessorKey: 'type', accessorKey: 'type',
@ -105,6 +86,11 @@ export function createFeedbackColumns(
</div> </div>
), ),
}, },
{
accessorKey: 'user.profile.full_name',
header: () => <span>Dibuat Oleh</span>,
cell: ({ row }) => row.original.user?.profile?.full_name ?? '-',
},
{ {
accessorKey: 'handler.profile.full_name', accessorKey: 'handler.profile.full_name',
header: () => <span>Ditindaklanjuti Oleh</span>, header: () => <span>Ditindaklanjuti Oleh</span>,

View File

@ -358,12 +358,12 @@ function ViewDetailDialog({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-3xl"> <DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-3xl">
<DialogHeader> <DialogHeader className="shrink-0">
<DialogTitle>Detail Kritik dan Saran</DialogTitle> <DialogTitle>Detail Kritik dan Saran</DialogTitle>
</DialogHeader> </DialogHeader>
{feedback && ( {feedback && (
<div className="grid gap-4 overflow-y-auto"> <div className="grid min-h-0 flex-1 gap-4 overflow-y-auto">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Badge variant="outline"> <Badge variant="outline">
{FeedbackTypeLabels[feedback.type]} {FeedbackTypeLabels[feedback.type]}
@ -391,6 +391,13 @@ function ViewDetailDialog({
<p className="font-medium">{feedback.subject}</p> <p className="font-medium">{feedback.subject}</p>
</div> </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"> <div className="grid gap-1">
<Label className="text-muted-foreground"> <Label className="text-muted-foreground">
Ditindaklanjuti Oleh Ditindaklanjuti Oleh

View File

@ -23,8 +23,15 @@ export type FeedbackHandler = {
profile: { full_name: string } | null; profile: { full_name: string } | null;
}; };
export type FeedbackUser = {
id: number;
profile: { full_name: string } | null;
};
export type Feedback = { export type Feedback = {
id: number; id: number;
user_id: number;
user: FeedbackUser | null;
type: FeedbackTypeValue; type: FeedbackTypeValue;
subject: string; subject: string;
message: string; message: string;