Compare commits
No commits in common. "daeeb3eb524b2a2afaf574ef197b70af87c3deac" and "ddbbf1cc7fcfa657c8477d9c2192e81486124d60" have entirely different histories.
daeeb3eb52
...
ddbbf1cc7f
@ -41,6 +41,18 @@ 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()
|
||||||
@ -49,18 +61,6 @@ 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([
|
||||||
|
|||||||
@ -9,7 +9,6 @@ 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;
|
||||||
@ -45,29 +44,23 @@ export function FormDialog({
|
|||||||
}: FormDialogProps) {
|
}: FormDialogProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<DialogContent className={contentClassName}>
|
||||||
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 className="shrink-0">
|
<DialogHeader>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid flex-1 gap-4 overflow-y-auto py-4">
|
<div className="grid gap-4 py-4">
|
||||||
{typeof children === 'function'
|
{typeof children === 'function'
|
||||||
? children({ errors, processing })
|
? children({ errors, processing })
|
||||||
: children}
|
: children}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="shrink-0">
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@ -31,6 +31,13 @@ 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>[] {
|
||||||
@ -49,6 +56,18 @@ 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',
|
||||||
@ -86,11 +105,6 @@ 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>,
|
||||||
|
|||||||
@ -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 className="shrink-0">
|
<DialogHeader>
|
||||||
<DialogTitle>Detail Kritik dan Saran</DialogTitle>
|
<DialogTitle>Detail Kritik dan Saran</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{feedback && (
|
{feedback && (
|
||||||
<div className="grid min-h-0 flex-1 gap-4 overflow-y-auto">
|
<div className="grid 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,13 +391,6 @@ 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
|
||||||
|
|||||||
@ -23,15 +23,8 @@ 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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user