feat: implement reply functionality for feedback; add ReplyFeedbackRequest and update FeedbackService; enhance UI for reply actions and display
This commit is contained in:
parent
900c27041a
commit
32c04b91dd
@ -6,6 +6,7 @@
|
|||||||
use App\Enums\FeedbackType;
|
use App\Enums\FeedbackType;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Feedback\FeedbackRequest;
|
use App\Http\Requests\Admin\Feedback\FeedbackRequest;
|
||||||
|
use App\Http\Requests\Admin\Feedback\ReplyFeedbackRequest;
|
||||||
use App\Http\Requests\Admin\Feedback\UpdateFeedbackStatusRequest;
|
use App\Http\Requests\Admin\Feedback\UpdateFeedbackStatusRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\Feedback;
|
use App\Models\Feedback;
|
||||||
@ -55,6 +56,7 @@ public function store(FeedbackRequest $request): RedirectResponse
|
|||||||
public function edit(Request $request, Feedback $feedback): Response
|
public function edit(Request $request, Feedback $feedback): Response
|
||||||
{
|
{
|
||||||
abort_unless($feedback->user_id === $request->user()->id, 403);
|
abort_unless($feedback->user_id === $request->user()->id, 403);
|
||||||
|
abort_unless($feedback->status === FeedbackStatus::Submitted, 403);
|
||||||
|
|
||||||
return Inertia::render('admin/feedback/edit', [
|
return Inertia::render('admin/feedback/edit', [
|
||||||
'feedback' => $feedback,
|
'feedback' => $feedback,
|
||||||
@ -64,8 +66,6 @@ public function edit(Request $request, Feedback $feedback): Response
|
|||||||
|
|
||||||
public function update(FeedbackRequest $request, Feedback $feedback): RedirectResponse
|
public function update(FeedbackRequest $request, Feedback $feedback): RedirectResponse
|
||||||
{
|
{
|
||||||
abort_unless($feedback->user_id === $request->user()->id, 403);
|
|
||||||
|
|
||||||
$this->service->update($feedback, $request->validated());
|
$this->service->update($feedback, $request->validated());
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Masukan berhasil diperbarui.']);
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Masukan berhasil diperbarui.']);
|
||||||
@ -76,6 +76,7 @@ public function update(FeedbackRequest $request, Feedback $feedback): RedirectRe
|
|||||||
public function destroy(Request $request, Feedback $feedback): RedirectResponse
|
public function destroy(Request $request, Feedback $feedback): RedirectResponse
|
||||||
{
|
{
|
||||||
abort_unless($feedback->user_id === $request->user()->id, 403);
|
abort_unless($feedback->user_id === $request->user()->id, 403);
|
||||||
|
abort_unless($feedback->status === FeedbackStatus::Submitted, 403);
|
||||||
|
|
||||||
$this->service->delete($feedback);
|
$this->service->delete($feedback);
|
||||||
|
|
||||||
@ -88,4 +89,11 @@ public function updateStatus(UpdateFeedbackStatusRequest $request, Feedback $fee
|
|||||||
|
|
||||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status masukan berhasil diperbarui.'])->back();
|
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status masukan berhasil diperbarui.'])->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function reply(ReplyFeedbackRequest $request, Feedback $feedback): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->service->reply($feedback, $request->validated('reply'));
|
||||||
|
|
||||||
|
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Balasan berhasil dikirim.'])->back();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ public function update(LetterRequestRequest $request, LetterRequest $letterReque
|
|||||||
|
|
||||||
public function destroy(LetterRequest $letterRequest): RedirectResponse
|
public function destroy(LetterRequest $letterRequest): RedirectResponse
|
||||||
{
|
{
|
||||||
abort_if($letterRequest->status !== LetterStatus::Submitted->value, 403);
|
abort_if($letterRequest->status !== LetterStatus::Submitted, 403);
|
||||||
|
|
||||||
$this->service->delete($letterRequest);
|
$this->service->delete($letterRequest);
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Admin\Feedback;
|
namespace App\Http\Requests\Admin\Feedback;
|
||||||
|
|
||||||
|
use App\Enums\FeedbackStatus;
|
||||||
use App\Enums\FeedbackType;
|
use App\Enums\FeedbackType;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@ -10,7 +11,15 @@ class FeedbackRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return $this->user()->can($this->isMethod('post') ? 'create-feedback' : 'update-feedback');
|
if ($this->isMethod('post')) {
|
||||||
|
return $this->user()->can('create-feedback');
|
||||||
|
}
|
||||||
|
|
||||||
|
$feedback = $this->route('feedback');
|
||||||
|
|
||||||
|
return $this->user()->can('update-feedback')
|
||||||
|
&& $feedback->user_id === $this->user()->id
|
||||||
|
&& $feedback->status === FeedbackStatus::Submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
|
|||||||
20
app/Http/Requests/Admin/Feedback/ReplyFeedbackRequest.php
Normal file
20
app/Http/Requests/Admin/Feedback/ReplyFeedbackRequest.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin\Feedback;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ReplyFeedbackRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user()->can('reply-feedback');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'reply' => ['required', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@ public function authorize(): bool
|
|||||||
|
|
||||||
return $this->user()->can('update-letter-requests')
|
return $this->user()->can('update-letter-requests')
|
||||||
&& $letterRequest->user_id === $this->user()->id
|
&& $letterRequest->user_id === $this->user()->id
|
||||||
&& $letterRequest->status === LetterStatus::Submitted->value;
|
&& $letterRequest->status === LetterStatus::Submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
|
|||||||
@ -21,6 +21,7 @@ protected function casts(): array
|
|||||||
return [
|
return [
|
||||||
'type' => FeedbackType::class,
|
'type' => FeedbackType::class,
|
||||||
'status' => FeedbackStatus::class,
|
'status' => FeedbackStatus::class,
|
||||||
|
'replied_at' => 'datetime',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,16 +43,19 @@ public function __construct()
|
|||||||
|
|
||||||
public function pendingCount(User $user): int
|
public function pendingCount(User $user): int
|
||||||
{
|
{
|
||||||
|
if (! $user->can('update-feedback-status')) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
return Feedback::query()
|
return Feedback::query()
|
||||||
->where('user_id', $user->id)
|
->where('status', FeedbackStatus::Submitted)
|
||||||
->where('status', '!=', FeedbackStatus::Resolved)
|
|
||||||
->count();
|
->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function paginated(User $user, int $perPage = 25, string $search = '', ?string $type = null, ?string $status = null): LengthAwarePaginator
|
public function paginated(User $user, int $perPage = 25, string $search = '', ?string $type = null, ?string $status = null): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return Feedback::query()
|
return Feedback::query()
|
||||||
->where('user_id', $user->id)
|
->when(! $user->can('update-feedback-status'), fn ($q) => $q->where('user_id', $user->id))
|
||||||
->with(['user.profile', 'handler.profile'])
|
->with(['user.profile', 'handler.profile'])
|
||||||
->when($search, fn ($q) => $q->where('subject', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('subject', 'like', "%{$search}%"))
|
||||||
->when($type, fn ($q) => $q->where('type', $type))
|
->when($type, fn ($q) => $q->where('type', $type))
|
||||||
@ -73,10 +76,11 @@ public function create(User $user, array $data): Feedback
|
|||||||
|
|
||||||
public function update(Feedback $feedback, array $data): Feedback
|
public function update(Feedback $feedback, array $data): Feedback
|
||||||
{
|
{
|
||||||
$feedback->type = $data['type'];
|
$feedback->update([
|
||||||
$feedback->subject = $data['subject'];
|
'type' => $data['type'],
|
||||||
$feedback->message = $this->sanitizer->sanitize($data['message']);
|
'subject' => $data['subject'],
|
||||||
$feedback->update();
|
'message' => $this->sanitizer->sanitize($data['message']),
|
||||||
|
]);
|
||||||
|
|
||||||
return $feedback;
|
return $feedback;
|
||||||
}
|
}
|
||||||
@ -91,6 +95,18 @@ public function updateStatus(Feedback $feedback, string $status): Feedback
|
|||||||
return $feedback;
|
return $feedback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function reply(Feedback $feedback, string $reply): Feedback
|
||||||
|
{
|
||||||
|
$feedback->update([
|
||||||
|
'reply' => $this->sanitizer->sanitize($reply),
|
||||||
|
'replied_at' => now(),
|
||||||
|
'handled_by' => auth()->id(),
|
||||||
|
'status' => FeedbackStatus::Resolved->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $feedback;
|
||||||
|
}
|
||||||
|
|
||||||
public function delete(Feedback $feedback): bool
|
public function delete(Feedback $feedback): bool
|
||||||
{
|
{
|
||||||
return $feedback->delete();
|
return $feedback->delete();
|
||||||
|
|||||||
@ -50,7 +50,7 @@ class PermissionCatalog
|
|||||||
];
|
];
|
||||||
|
|
||||||
public const FEEDBACK = [
|
public const FEEDBACK = [
|
||||||
'view-feedback', 'create-feedback', 'update-feedback', 'delete-feedback', 'update-feedback-status',
|
'view-feedback', 'create-feedback', 'update-feedback', 'delete-feedback', 'update-feedback-status', 'reply-feedback',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -18,7 +18,7 @@ public function up(): void
|
|||||||
$table->text('message');
|
$table->text('message');
|
||||||
$table->enum('status', FeedbackStatus::values())->default(FeedbackStatus::Submitted->value);
|
$table->enum('status', FeedbackStatus::values())->default(FeedbackStatus::Submitted->value);
|
||||||
$table->foreignId('handled_by')->nullable()->constrained('users')->nullOnDelete();
|
$table->foreignId('handled_by')->nullable()->constrained('users')->nullOnDelete();
|
||||||
$table->text('admin_notes')->nullable();
|
$table->text('reply')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,8 +91,9 @@ public function run(): void
|
|||||||
...$manage,
|
...$manage,
|
||||||
...array_diff($services, ['create-letter-requests', 'update-letter-requests']),
|
...array_diff($services, ['create-letter-requests', 'update-letter-requests']),
|
||||||
...$users,
|
...$users,
|
||||||
...$feedbackSelfService,
|
'view-feedback',
|
||||||
'update-feedback-status',
|
'update-feedback-status',
|
||||||
|
'reply-feedback',
|
||||||
],
|
],
|
||||||
'staff-keuangan' => [
|
'staff-keuangan' => [
|
||||||
'view-dashboard',
|
'view-dashboard',
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Eye, Pencil, Trash2 } from 'lucide-react';
|
import { Eye, MessageSquareReply, Pencil, Trash2 } from 'lucide-react';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { StatusBadge } from '@/components/status-badge';
|
import { StatusBadge } from '@/components/status-badge';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@ -16,10 +16,12 @@ type CreateColumnsParams = {
|
|||||||
handleEdit: (feedback: Feedback) => void;
|
handleEdit: (feedback: Feedback) => void;
|
||||||
handleDeleteClick: (feedback: Feedback) => void;
|
handleDeleteClick: (feedback: Feedback) => void;
|
||||||
handleStatusChange: (feedback: Feedback, status: string) => void;
|
handleStatusChange: (feedback: Feedback, status: string) => void;
|
||||||
|
handleReply: (feedback: Feedback) => void;
|
||||||
statuses: StatusOption[];
|
statuses: StatusOption[];
|
||||||
canUpdateStatus: boolean;
|
canUpdateStatus: boolean;
|
||||||
canUpdate: boolean;
|
canUpdate: boolean;
|
||||||
canDelete: boolean;
|
canDelete: boolean;
|
||||||
|
canReply: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FeedbackStatusVariants: Record<
|
export const FeedbackStatusVariants: Record<
|
||||||
@ -39,10 +41,12 @@ export function createFeedbackColumns(
|
|||||||
handleEdit,
|
handleEdit,
|
||||||
handleDeleteClick,
|
handleDeleteClick,
|
||||||
handleStatusChange,
|
handleStatusChange,
|
||||||
|
handleReply,
|
||||||
statuses,
|
statuses,
|
||||||
canUpdateStatus,
|
canUpdateStatus,
|
||||||
canUpdate,
|
canUpdate,
|
||||||
canDelete,
|
canDelete,
|
||||||
|
canReply,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -109,31 +113,45 @@ export function createFeedbackColumns(
|
|||||||
className: 'w-[100px] text-center',
|
className: 'w-[100px] text-center',
|
||||||
headerClassName: 'w-[100px] text-center',
|
headerClassName: 'w-[100px] text-center',
|
||||||
},
|
},
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<RowActions
|
const isLocked = row.original.status !== 'submitted';
|
||||||
actions={[
|
|
||||||
{
|
return (
|
||||||
label: 'Lihat Detail',
|
<RowActions
|
||||||
icon: <Eye className="h-4 w-4" />,
|
actions={[
|
||||||
onClick: () => handleView(row.original),
|
{
|
||||||
},
|
label: 'Lihat Detail',
|
||||||
{
|
icon: <Eye className="h-4 w-4" />,
|
||||||
label: 'Edit',
|
onClick: () => handleView(row.original),
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
},
|
||||||
show: canUpdate,
|
{
|
||||||
onClick: () => handleEdit(row.original),
|
label: 'Balas',
|
||||||
},
|
icon: (
|
||||||
{
|
<MessageSquareReply className="h-4 w-4" />
|
||||||
label: 'Hapus',
|
),
|
||||||
icon: (
|
show:
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
canReply &&
|
||||||
),
|
row.original.status === 'in_review',
|
||||||
show: canDelete,
|
onClick: () => handleReply(row.original),
|
||||||
onClick: () => handleDeleteClick(row.original),
|
},
|
||||||
},
|
{
|
||||||
]}
|
label: 'Edit',
|
||||||
/>
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
),
|
show: canUpdate && !isLocked,
|
||||||
|
onClick: () => handleEdit(row.original),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hapus',
|
||||||
|
icon: (
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
),
|
||||||
|
show: canDelete && !isLocked,
|
||||||
|
onClick: () => handleDeleteClick(row.original),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,10 @@ import { DataTable } from '@/components/data-table';
|
|||||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
import type { FilterField } from '@/components/filter-dialog';
|
import type { FilterField } from '@/components/filter-dialog';
|
||||||
import { FilterDialog } from '@/components/filter-dialog';
|
import { FilterDialog } from '@/components/filter-dialog';
|
||||||
|
import { FormDialog } from '@/components/form-dialog';
|
||||||
|
import InputError from '@/components/input-error';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
|
import TiptapEditor from '@/components/rich-text-editor';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -23,6 +26,7 @@ import {
|
|||||||
destroy,
|
destroy,
|
||||||
edit,
|
edit,
|
||||||
index as feedbackIndex,
|
index as feedbackIndex,
|
||||||
|
reply as replyRoute,
|
||||||
update_status,
|
update_status,
|
||||||
} from '@/routes/admin/feedback';
|
} from '@/routes/admin/feedback';
|
||||||
import type { Feedback } from '@/types/feedback';
|
import type { Feedback } from '@/types/feedback';
|
||||||
@ -59,11 +63,13 @@ export default function FeedbackIndex({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const [deleting, setDeleting] = useState<Feedback | null>(null);
|
const [deleting, setDeleting] = useState<Feedback | null>(null);
|
||||||
const [viewing, setViewing] = useState<Feedback | null>(null);
|
const [viewing, setViewing] = useState<Feedback | null>(null);
|
||||||
|
const [replying, setReplying] = useState<Feedback | null>(null);
|
||||||
const { hasPermission } = usePermissions();
|
const { hasPermission } = usePermissions();
|
||||||
const canCreate = hasPermission('create-feedback');
|
const canCreate = hasPermission('create-feedback');
|
||||||
const canUpdate = hasPermission('update-feedback');
|
const canUpdate = hasPermission('update-feedback');
|
||||||
const canDelete = hasPermission('delete-feedback');
|
const canDelete = hasPermission('delete-feedback');
|
||||||
const canUpdateStatus = hasPermission('update-feedback-status');
|
const canUpdateStatus = hasPermission('update-feedback-status');
|
||||||
|
const canReply = hasPermission('reply-feedback');
|
||||||
|
|
||||||
const filterFields: FilterField[] = [
|
const filterFields: FilterField[] = [
|
||||||
{
|
{
|
||||||
@ -120,10 +126,12 @@ export default function FeedbackIndex({
|
|||||||
handleEdit: (feedback) => router.get(edit.url(feedback.id)),
|
handleEdit: (feedback) => router.get(edit.url(feedback.id)),
|
||||||
handleDeleteClick: (feedback) => setDeleting(feedback),
|
handleDeleteClick: (feedback) => setDeleting(feedback),
|
||||||
handleStatusChange,
|
handleStatusChange,
|
||||||
|
handleReply: (feedback) => setReplying(feedback),
|
||||||
statuses,
|
statuses,
|
||||||
canUpdateStatus,
|
canUpdateStatus,
|
||||||
canUpdate,
|
canUpdate,
|
||||||
canDelete,
|
canDelete,
|
||||||
|
canReply,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -155,6 +163,17 @@ export default function FeedbackIndex({
|
|||||||
feedback={viewing}
|
feedback={viewing}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ReplyForm
|
||||||
|
key={replying?.id}
|
||||||
|
open={replying !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setReplying(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
feedback={replying}
|
||||||
|
/>
|
||||||
|
|
||||||
{canUpdateStatus && (
|
{canUpdateStatus && (
|
||||||
<Alert>
|
<Alert>
|
||||||
<Info />
|
<Info />
|
||||||
@ -202,6 +221,57 @@ export default function FeedbackIndex({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ReplyForm({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
feedback,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
feedback: Feedback | null;
|
||||||
|
}) {
|
||||||
|
const [reply, setReply] = useState(feedback?.reply ?? '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title="Balas Kritik dan Saran"
|
||||||
|
action={feedback ? replyRoute(feedback.id) : ''}
|
||||||
|
resetOnSuccess
|
||||||
|
onSuccess={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{({ errors }) =>
|
||||||
|
feedback && (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label className="text-muted-foreground">
|
||||||
|
Subjek
|
||||||
|
</Label>
|
||||||
|
<p className="font-medium">{feedback.subject}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
Balasan{' '}
|
||||||
|
<span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<TiptapEditor
|
||||||
|
name="reply"
|
||||||
|
value={reply}
|
||||||
|
onChange={setReply}
|
||||||
|
placeholder="Tulis balasan untuk pengirim"
|
||||||
|
error={errors.reply}
|
||||||
|
/>
|
||||||
|
<InputError message={errors.reply} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</FormDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ViewDetailDialog({
|
function ViewDetailDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
@ -279,14 +349,22 @@ function ViewDetailDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{feedback.admin_notes && (
|
{feedback.reply && (
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<Label className="text-muted-foreground">
|
<Label className="text-muted-foreground">
|
||||||
Catatan Admin
|
Balasan
|
||||||
|
{feedback.replied_at &&
|
||||||
|
` · ${format(new Date(feedback.replied_at), 'd MMM yyyy, HH:mm')}`}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="rounded-md border bg-muted/50 p-3 text-sm whitespace-pre-line">
|
<div
|
||||||
{feedback.admin_notes}
|
className={cn(
|
||||||
</p>
|
'rounded-md border bg-muted/50 p-3 text-sm',
|
||||||
|
richTextContentClass,
|
||||||
|
)}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: feedback.reply,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -36,7 +36,8 @@ export type Feedback = {
|
|||||||
subject: string;
|
subject: string;
|
||||||
message: string;
|
message: string;
|
||||||
status: FeedbackStatusValue;
|
status: FeedbackStatusValue;
|
||||||
admin_notes: string | null;
|
reply: string | null;
|
||||||
|
replied_at: string | null;
|
||||||
handled_by: number | null;
|
handled_by: number | null;
|
||||||
handler: FeedbackHandler | null;
|
handler: FeedbackHandler | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|||||||
@ -194,6 +194,7 @@
|
|||||||
->middlewareFor(['edit', 'update'], 'permission:update-feedback')
|
->middlewareFor(['edit', 'update'], 'permission:update-feedback')
|
||||||
->middlewareFor(['destroy'], 'permission:delete-feedback');
|
->middlewareFor(['destroy'], 'permission:delete-feedback');
|
||||||
Route::patch('admin/feedback/{feedback}/status', [FeedbackController::class, 'updateStatus'])->name('admin.feedback.update_status')->middleware('permission:update-feedback-status');
|
Route::patch('admin/feedback/{feedback}/status', [FeedbackController::class, 'updateStatus'])->name('admin.feedback.update_status')->middleware('permission:update-feedback-status');
|
||||||
|
Route::patch('admin/feedback/{feedback}/reply', [FeedbackController::class, 'reply'])->name('admin.feedback.reply')->middleware('permission:reply-feedback');
|
||||||
|
|
||||||
Route::prefix('admin/users')->name('admin.users.')->group(function () {
|
Route::prefix('admin/users')->name('admin.users.')->group(function () {
|
||||||
Route::resource('lecturers', LecturerController::class)
|
Route::resource('lecturers', LecturerController::class)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user