Compare commits
No commits in common. "ddbbf1cc7fcfa657c8477d9c2192e81486124d60" and "86ea75ec91e16a2353efa66ace6818a257db3428" have entirely different histories.
ddbbf1cc7f
...
86ea75ec91
@ -5,8 +5,7 @@
|
|||||||
use App\Enums\FeedbackStatus;
|
use App\Enums\FeedbackStatus;
|
||||||
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\FeedbackRequest;
|
||||||
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;
|
||||||
use App\Services\Admin\FeedbackService;
|
use App\Services\Admin\FeedbackService;
|
||||||
@ -56,13 +55,6 @@ public function update(FeedbackRequest $request, Feedback $feedback): RedirectRe
|
|||||||
return to_route('admin.feedback.index');
|
return to_route('admin.feedback.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateStatus(UpdateFeedbackStatusRequest $request, Feedback $feedback): RedirectResponse
|
|
||||||
{
|
|
||||||
$this->service->updateStatus($feedback, $request->validated('status'));
|
|
||||||
|
|
||||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Status masukan berhasil diperbarui.'])->back();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
use App\Services\Admin\FeedbackService;
|
|
||||||
use App\Services\NotificationService;
|
use App\Services\NotificationService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@ -49,9 +48,6 @@ public function share(Request $request): array
|
|||||||
'unreadNotificationsCount' => fn () => $request->user()
|
'unreadNotificationsCount' => fn () => $request->user()
|
||||||
? app(NotificationService::class)->unreadCount($request->user())
|
? app(NotificationService::class)->unreadCount($request->user())
|
||||||
: 0,
|
: 0,
|
||||||
'pendingFeedbackCount' => fn () => $request->user()
|
|
||||||
? app(FeedbackService::class)->pendingCount($request->user())
|
|
||||||
: 0,
|
|
||||||
'notifications' => Inertia::merge(fn () => $request->user()
|
'notifications' => Inertia::merge(fn () => $request->user()
|
||||||
? app(NotificationService::class)->paginated($request->user())
|
? app(NotificationService::class)->paginated($request->user())
|
||||||
: null)->append('data', 'id'),
|
: null)->append('data', 'id'),
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests\Admin\Feedback;
|
|
||||||
|
|
||||||
use App\Enums\FeedbackStatus;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class UpdateFeedbackStatusRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return $this->user()->can('update-feedback-status');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'status' => ['required', Rule::enum(FeedbackStatus::class)],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Requests\Admin\Feedback;
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
use App\Enums\FeedbackType;
|
use App\Enums\FeedbackType;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
@ -10,7 +10,7 @@ class FeedbackRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return $this->user()->can($this->isMethod('post') ? 'create-feedback' : 'update-feedback');
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
@ -28,9 +28,4 @@ public function user(): BelongsTo
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handler(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'handled_by');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin;
|
namespace App\Services\Admin;
|
||||||
|
|
||||||
use App\Enums\FeedbackStatus;
|
|
||||||
use App\Models\Feedback;
|
use App\Models\Feedback;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Support\TextAlignAttributeSanitizer;
|
use App\Support\TextAlignAttributeSanitizer;
|
||||||
@ -45,22 +44,13 @@ public function paginated(User $user, int $perPage = 25, string $search = '', ?s
|
|||||||
{
|
{
|
||||||
return Feedback::query()
|
return Feedback::query()
|
||||||
->where('user_id', $user->id)
|
->where('user_id', $user->id)
|
||||||
->with('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))
|
->when($status, fn ($q) => $q->where('status', $status))
|
||||||
->when($status, fn($q) => $q->where('status', $status))
|
|
||||||
->latest()
|
->latest()
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pendingCount(User $user): int
|
|
||||||
{
|
|
||||||
return Feedback::query()
|
|
||||||
->where('user_id', $user->id)
|
|
||||||
->where('status', '!=', FeedbackStatus::Resolved)
|
|
||||||
->count();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(User $user, array $data): Feedback
|
public function create(User $user, array $data): Feedback
|
||||||
{
|
{
|
||||||
return Feedback::create([
|
return Feedback::create([
|
||||||
@ -81,16 +71,6 @@ public function update(Feedback $feedback, array $data): Feedback
|
|||||||
return $feedback;
|
return $feedback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateStatus(Feedback $feedback, string $status): Feedback
|
|
||||||
{
|
|
||||||
$feedback->update([
|
|
||||||
'status' => $status,
|
|
||||||
'handled_by' => auth()->id(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $feedback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(Feedback $feedback): bool
|
public function delete(Feedback $feedback): bool
|
||||||
{
|
{
|
||||||
return $feedback->delete();
|
return $feedback->delete();
|
||||||
|
|||||||
@ -48,10 +48,6 @@ class PermissionCatalog
|
|||||||
'view-roles', 'update-roles',
|
'view-roles', 'update-roles',
|
||||||
];
|
];
|
||||||
|
|
||||||
public const FEEDBACK = [
|
|
||||||
'view-feedback', 'create-feedback', 'update-feedback', 'delete-feedback', 'update-feedback-status',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, array<int, string>>
|
* @return array<string, array<int, string>>
|
||||||
*/
|
*/
|
||||||
@ -65,7 +61,6 @@ public static function groups(): array
|
|||||||
'Keuangan' => self::FINANCES,
|
'Keuangan' => self::FINANCES,
|
||||||
'Layanan' => self::SERVICES,
|
'Layanan' => self::SERVICES,
|
||||||
'Pengguna' => self::USERS,
|
'Pengguna' => self::USERS,
|
||||||
'Kritik & Saran' => self::FEEDBACK,
|
|
||||||
'Pengembang' => self::DEVELOPER,
|
'Pengembang' => self::DEVELOPER,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,6 @@ public function up(): void
|
|||||||
$table->string('subject', 150);
|
$table->string('subject', 150);
|
||||||
$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->text('admin_notes')->nullable();
|
$table->text('admin_notes')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('notifications', function (Blueprint $table) {
|
||||||
|
$table->foreignId('created_by')->nullable()->after('user_id')->constrained('users')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('notifications', function (Blueprint $table) {
|
||||||
|
$table->dropConstrainedForeignId('created_by');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -21,10 +21,6 @@ public function run(): void
|
|||||||
$services = PermissionCatalog::SERVICES;
|
$services = PermissionCatalog::SERVICES;
|
||||||
$users = PermissionCatalog::USERS;
|
$users = PermissionCatalog::USERS;
|
||||||
|
|
||||||
$feedbackSelfService = [
|
|
||||||
'view-feedback', 'create-feedback', 'update-feedback', 'delete-feedback',
|
|
||||||
];
|
|
||||||
|
|
||||||
$permissionNames = PermissionCatalog::all();
|
$permissionNames = PermissionCatalog::all();
|
||||||
|
|
||||||
foreach ($permissionNames as $name) {
|
foreach ($permissionNames as $name) {
|
||||||
@ -34,8 +30,8 @@ public function run(): void
|
|||||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||||
|
|
||||||
$roles = [
|
$roles = [
|
||||||
'mahasiswa' => ['view-dashboard', ...$feedbackSelfService],
|
'mahasiswa' => ['view-dashboard'],
|
||||||
'dosen' => ['view-dashboard', ...$feedbackSelfService],
|
'dosen' => ['view-dashboard'],
|
||||||
'staff-admin' => [
|
'staff-admin' => [
|
||||||
'view-dashboard',
|
'view-dashboard',
|
||||||
...$master,
|
...$master,
|
||||||
@ -43,13 +39,10 @@ public function run(): void
|
|||||||
...$manage,
|
...$manage,
|
||||||
...$services,
|
...$services,
|
||||||
...$users,
|
...$users,
|
||||||
...$feedbackSelfService,
|
|
||||||
'update-feedback-status',
|
|
||||||
],
|
],
|
||||||
'staff-keuangan' => [
|
'staff-keuangan' => [
|
||||||
'view-dashboard',
|
'view-dashboard',
|
||||||
...$finances,
|
...$finances,
|
||||||
...$feedbackSelfService,
|
|
||||||
],
|
],
|
||||||
'kaprodi' => [
|
'kaprodi' => [
|
||||||
'view-dashboard',
|
'view-dashboard',
|
||||||
@ -58,7 +51,6 @@ public function run(): void
|
|||||||
'view-course-classes',
|
'view-course-classes',
|
||||||
'view-course-registrations',
|
'view-course-registrations',
|
||||||
'view-students',
|
'view-students',
|
||||||
...$feedbackSelfService,
|
|
||||||
],
|
],
|
||||||
'developer' => $permissionNames,
|
'developer' => $permissionNames,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -224,27 +224,21 @@ function buildNavMain({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNavSecondary(
|
const navSecondary: { title: string; url: string; icon: Icon }[] = [
|
||||||
pendingFeedbackCount: number,
|
{
|
||||||
): { title: string; url: string; icon: Icon; badge?: number }[] {
|
title: 'Kritik dan Saran',
|
||||||
return [
|
url: feedbackRoute.url(),
|
||||||
{
|
icon: IconMessageDots,
|
||||||
title: 'Kritik dan Saran',
|
},
|
||||||
url: feedbackRoute.url(),
|
{
|
||||||
icon: IconMessageDots,
|
title: 'Bantuan',
|
||||||
badge: pendingFeedbackCount,
|
url: '#',
|
||||||
},
|
icon: IconHelp,
|
||||||
{
|
},
|
||||||
title: 'Bantuan',
|
];
|
||||||
url: '#',
|
|
||||||
icon: IconHelp,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
const { name, auth, pendingFeedbackCount } = usePage<{ auth: Auth }>()
|
const { name, auth } = usePage<{ auth: Auth }>().props;
|
||||||
.props;
|
|
||||||
const roleNames = auth?.user?.roles?.map((role) => role.name) ?? [];
|
const roleNames = auth?.user?.roles?.map((role) => role.name) ?? [];
|
||||||
const isStaff = roleNames.some((role) => STAFF_ROLES.includes(role));
|
const isStaff = roleNames.some((role) => STAFF_ROLES.includes(role));
|
||||||
const isMahasiswa = !isStaff && roleNames.includes('mahasiswa');
|
const isMahasiswa = !isStaff && roleNames.includes('mahasiswa');
|
||||||
@ -279,10 +273,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<NavMain items={navMain} />
|
<NavMain items={navMain} />
|
||||||
<NavSecondary
|
<NavSecondary items={navSecondary} className="mt-auto" />
|
||||||
items={buildNavSecondary(pendingFeedbackCount ?? 0)}
|
|
||||||
className="mt-auto"
|
|
||||||
/>
|
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -20,13 +20,12 @@ type FormDialogProps = {
|
|||||||
submitDisabled?: boolean;
|
submitDisabled?: boolean;
|
||||||
submitLabel?: ReactNode;
|
submitLabel?: ReactNode;
|
||||||
submittingLabel?: ReactNode;
|
submittingLabel?: ReactNode;
|
||||||
contentClassName?: string;
|
|
||||||
children:
|
children:
|
||||||
| ReactNode
|
| ReactNode
|
||||||
| ((ctx: {
|
| ((ctx: {
|
||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
}) => ReactNode);
|
}) => ReactNode);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FormDialog({
|
export function FormDialog({
|
||||||
@ -39,12 +38,11 @@ export function FormDialog({
|
|||||||
submitDisabled = false,
|
submitDisabled = false,
|
||||||
submitLabel = 'Simpan',
|
submitLabel = 'Simpan',
|
||||||
submittingLabel = 'Menyimpan...',
|
submittingLabel = 'Menyimpan...',
|
||||||
contentClassName,
|
|
||||||
children,
|
children,
|
||||||
}: FormDialogProps) {
|
}: FormDialogProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className={contentClassName}>
|
<DialogContent>
|
||||||
<Form
|
<Form
|
||||||
action={action}
|
action={action}
|
||||||
resetOnSuccess={resetOnSuccess}
|
resetOnSuccess={resetOnSuccess}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import {
|
|||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuBadge,
|
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
@ -21,7 +20,6 @@ export function NavSecondary({
|
|||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon: Icon;
|
icon: Icon;
|
||||||
badge?: number;
|
|
||||||
}[];
|
}[];
|
||||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||||
return (
|
return (
|
||||||
@ -36,11 +34,6 @@ export function NavSecondary({
|
|||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
{!!item.badge && (
|
|
||||||
<SidebarMenuBadge>
|
|
||||||
{item.badge}
|
|
||||||
</SidebarMenuBadge>
|
|
||||||
)}
|
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|||||||
@ -9,29 +9,34 @@ import {
|
|||||||
|
|
||||||
type StatusOption = { value: string; label: string };
|
type StatusOption = { value: string; label: string };
|
||||||
|
|
||||||
type StatusBadgeProps = {
|
type StudentStatusBadgeProps = {
|
||||||
status: string | null | undefined;
|
status: string | null | undefined;
|
||||||
statuses: StatusOption[];
|
statuses: StatusOption[];
|
||||||
onChange: (status: string) => void;
|
onChange: (status: string) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
variants?: Record<
|
|
||||||
string,
|
|
||||||
'default' | 'secondary' | 'destructive' | 'outline'
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function StatusBadge({
|
const StudentStatusVariants: Record<
|
||||||
|
string,
|
||||||
|
'default' | 'secondary' | 'destructive' | 'outline'
|
||||||
|
> = {
|
||||||
|
active: 'default',
|
||||||
|
on_leave: 'secondary',
|
||||||
|
graduated: 'outline',
|
||||||
|
dropped_out: 'destructive',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StudentStatusBadge({
|
||||||
status,
|
status,
|
||||||
statuses,
|
statuses,
|
||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
variants,
|
}: StudentStatusBadgeProps) {
|
||||||
}: StatusBadgeProps) {
|
|
||||||
const label =
|
const label =
|
||||||
statuses.find((option) => option.value === status)?.label ??
|
statuses.find((option) => option.value === status)?.label ??
|
||||||
status ??
|
status ??
|
||||||
'-';
|
'-';
|
||||||
const variant = (status && variants?.[status]) || 'outline';
|
const variant = (status && StudentStatusVariants[status]) || 'outline';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
import { Head, router } from '@inertiajs/react';
|
||||||
|
import { Clock, MapPin, Pencil, Plus, Trash2, Video } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
import { FormDialog } from '@/components/form-dialog';
|
import { FormDialog } from '@/components/form-dialog';
|
||||||
import InputError from '@/components/input-error';
|
import InputError from '@/components/input-error';
|
||||||
@ -24,9 +27,6 @@ import {
|
|||||||
} from '@/routes/admin/academic-classes/schedules';
|
} from '@/routes/admin/academic-classes/schedules';
|
||||||
import type { Schedule } from '@/types/schedule';
|
import type { Schedule } from '@/types/schedule';
|
||||||
import { DayOfWeekLabels, DaysOfWeek } from '@/types/schedule';
|
import { DayOfWeekLabels, DaysOfWeek } from '@/types/schedule';
|
||||||
import { Head, router } from '@inertiajs/react';
|
|
||||||
import { Clock, MapPin, Pencil, Plus, Trash2, Video } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
type CourseClassOption = {
|
type CourseClassOption = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -108,10 +108,6 @@ export default function ScheduleIndex({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="hidden text-xs text-muted-foreground md:block">
|
|
||||||
Geser ke samping untuk melihat hari lainnya.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<CreateForm
|
<CreateForm
|
||||||
open={createOpen}
|
open={createOpen}
|
||||||
onOpenChange={setCreateOpen}
|
onOpenChange={setCreateOpen}
|
||||||
@ -135,14 +131,14 @@ export default function ScheduleIndex({
|
|||||||
Belum ada data jadwal.
|
Belum ada data jadwal.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 flex-col gap-4 pb-2 md:flex-row md:overflow-x-auto">
|
<div className="flex flex-1 gap-4 overflow-x-auto pb-2">
|
||||||
{columns.map((day) => {
|
{columns.map((day) => {
|
||||||
const items = grouped.get(day) ?? [];
|
const items = grouped.get(day) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={day}
|
key={day}
|
||||||
className="flex w-full shrink-0 flex-col gap-3 border-b pb-6 last:border-b-0 last:pb-0 md:w-72 md:border-b-0 md:pb-0"
|
className="flex w-72 shrink-0 flex-col gap-3"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between rounded-md bg-muted px-3 py-2">
|
<div className="flex items-center justify-between rounded-md bg-muted px-3 py-2">
|
||||||
<span className="text-sm font-semibold">
|
<span className="text-sm font-semibold">
|
||||||
@ -167,8 +163,8 @@ export default function ScheduleIndex({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'gap-3 py-4',
|
'gap-3 py-4',
|
||||||
highlight ===
|
highlight ===
|
||||||
schedule.id &&
|
schedule.id &&
|
||||||
'ring-2 ring-primary',
|
'ring-2 ring-primary',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CardHeader className="flex flex-row items-start justify-between gap-2 px-4">
|
<CardHeader className="flex flex-row items-start justify-between gap-2 px-4">
|
||||||
@ -213,17 +209,17 @@ export default function ScheduleIndex({
|
|||||||
<CardContent className="flex flex-col gap-1.5 px-4 text-xs text-muted-foreground">
|
<CardContent className="flex flex-col gap-1.5 px-4 text-xs text-muted-foreground">
|
||||||
{(schedule.start_time ||
|
{(schedule.start_time ||
|
||||||
schedule.end_time) && (
|
schedule.end_time) && (
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
<Clock className="h-3.5 w-3.5 shrink-0" />
|
<Clock className="h-3.5 w-3.5 shrink-0" />
|
||||||
{toTimeInput(
|
{toTimeInput(
|
||||||
schedule.start_time,
|
schedule.start_time,
|
||||||
) || '-'}{' '}
|
) || '-'}{' '}
|
||||||
-{' '}
|
-{' '}
|
||||||
{toTimeInput(
|
{toTimeInput(
|
||||||
schedule.end_time,
|
schedule.end_time,
|
||||||
) || '-'}
|
) || '-'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{schedule.room && (
|
{schedule.room && (
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
<MapPin className="h-3.5 w-3.5 shrink-0" />
|
<MapPin className="h-3.5 w-3.5 shrink-0" />
|
||||||
|
|||||||
@ -1,34 +1,16 @@
|
|||||||
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 { Pencil, Trash2 } from 'lucide-react';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { StatusBadge } from '@/components/status-badge';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import type { Feedback } from '@/types/feedback';
|
import type { Feedback, FeedbackStatusValue } from '@/types/feedback';
|
||||||
import { FeedbackTypeLabels } from '@/types/feedback';
|
import { FeedbackStatusLabels, FeedbackTypeLabels } from '@/types/feedback';
|
||||||
|
|
||||||
export type { Feedback } from '@/types/feedback';
|
export type { Feedback } from '@/types/feedback';
|
||||||
|
|
||||||
type StatusOption = { value: string; label: string };
|
|
||||||
|
|
||||||
type CreateColumnsParams = {
|
type CreateColumnsParams = {
|
||||||
handleView: (feedback: Feedback) => void;
|
|
||||||
handleEdit: (feedback: Feedback) => void;
|
handleEdit: (feedback: Feedback) => void;
|
||||||
handleDeleteClick: (feedback: Feedback) => void;
|
handleDeleteClick: (feedback: Feedback) => void;
|
||||||
handleStatusChange: (feedback: Feedback, status: string) => void;
|
|
||||||
statuses: StatusOption[];
|
|
||||||
canUpdateStatus: boolean;
|
|
||||||
canUpdate: boolean;
|
|
||||||
canDelete: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FeedbackStatusVariants: Record<
|
|
||||||
string,
|
|
||||||
'default' | 'secondary' | 'destructive' | 'outline'
|
|
||||||
> = {
|
|
||||||
submitted: 'outline',
|
|
||||||
in_review: 'secondary',
|
|
||||||
resolved: 'default',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function stripHtml(html: string): string {
|
function stripHtml(html: string): string {
|
||||||
@ -38,19 +20,24 @@ function stripHtml(html: string): string {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function statusBadgeVariant(
|
||||||
|
status: FeedbackStatusValue,
|
||||||
|
): 'outline' | 'secondary' | 'default' {
|
||||||
|
if (status === 'resolved') {
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'in_review') {
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'outline';
|
||||||
|
}
|
||||||
|
|
||||||
export function createFeedbackColumns(
|
export function createFeedbackColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<Feedback>[] {
|
): ColumnDef<Feedback>[] {
|
||||||
const {
|
const { handleEdit, handleDeleteClick } = params;
|
||||||
handleView,
|
|
||||||
handleEdit,
|
|
||||||
handleDeleteClick,
|
|
||||||
handleStatusChange,
|
|
||||||
statuses,
|
|
||||||
canUpdateStatus,
|
|
||||||
canUpdate,
|
|
||||||
canDelete,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -93,23 +80,12 @@ export function createFeedbackColumns(
|
|||||||
},
|
},
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<StatusBadge
|
<Badge variant={statusBadgeVariant(row.original.status)}>
|
||||||
status={row.original.status}
|
{FeedbackStatusLabels[row.original.status]}
|
||||||
statuses={statuses}
|
</Badge>
|
||||||
variants={FeedbackStatusVariants}
|
|
||||||
onChange={(status) =>
|
|
||||||
handleStatusChange(row.original, status)
|
|
||||||
}
|
|
||||||
disabled={!canUpdateStatus}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'handler.profile.full_name',
|
|
||||||
header: () => <span>Ditindaklanjuti Oleh</span>,
|
|
||||||
cell: ({ row }) => row.original.handler?.profile?.full_name ?? '-',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: 'created_at',
|
accessorKey: 'created_at',
|
||||||
header: () => <span>Dikirim</span>,
|
header: () => <span>Dikirim</span>,
|
||||||
@ -126,15 +102,9 @@ export function createFeedbackColumns(
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<RowActions
|
<RowActions
|
||||||
actions={[
|
actions={[
|
||||||
{
|
|
||||||
label: 'Lihat Detail',
|
|
||||||
icon: <Eye className="h-4 w-4" />,
|
|
||||||
onClick: () => handleView(row.original),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show: canUpdate,
|
|
||||||
onClick: () => handleEdit(row.original),
|
onClick: () => handleEdit(row.original),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -142,7 +112,6 @@ export function createFeedbackColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show: canDelete,
|
|
||||||
onClick: () => handleDeleteClick(row.original),
|
onClick: () => handleDeleteClick(row.original),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { Head, router } from '@inertiajs/react';
|
import { Head, router } from '@inertiajs/react';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { PaginationState } from '@/components/data-table';
|
import type { PaginationState } from '@/components/data-table';
|
||||||
@ -11,14 +10,7 @@ import { FormDialog } from '@/components/form-dialog';
|
|||||||
import InputError from '@/components/input-error';
|
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 TiptapEditor from '@/components/rich-text-editor';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import {
|
import {
|
||||||
@ -28,18 +20,15 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { usePermissions } from '@/hooks/use-permissions';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
index as feedbackIndex,
|
index as feedbackIndex,
|
||||||
destroy,
|
destroy,
|
||||||
store,
|
store,
|
||||||
update,
|
update,
|
||||||
update_status,
|
|
||||||
} from '@/routes/admin/feedback';
|
} from '@/routes/admin/feedback';
|
||||||
import type { Feedback } from '@/types/feedback';
|
import type { Feedback } from '@/types/feedback';
|
||||||
import { FeedbackStatusLabels, FeedbackTypeLabels } from '@/types/feedback';
|
import { createFeedbackColumns } from './columns';
|
||||||
import { createFeedbackColumns, FeedbackStatusVariants } from './columns';
|
|
||||||
|
|
||||||
type FeedbackTypeOption = { value: string; label: string };
|
type FeedbackTypeOption = { value: string; label: string };
|
||||||
|
|
||||||
@ -68,12 +57,6 @@ export default function FeedbackIndex({
|
|||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<Feedback | null>(null);
|
const [editing, setEditing] = useState<Feedback | null>(null);
|
||||||
const [deleting, setDeleting] = useState<Feedback | null>(null);
|
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[] = [
|
const filterFields: FilterField[] = [
|
||||||
{
|
{
|
||||||
@ -117,23 +100,9 @@ export default function FeedbackIndex({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStatusChange(feedback: Feedback, status: string) {
|
|
||||||
router.patch(
|
|
||||||
update_status.url(feedback.id),
|
|
||||||
{ status },
|
|
||||||
{ preserveScroll: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = createFeedbackColumns({
|
const columns = createFeedbackColumns({
|
||||||
handleView: (feedback) => setViewing(feedback),
|
|
||||||
handleEdit: (feedback) => setEditing(feedback),
|
handleEdit: (feedback) => setEditing(feedback),
|
||||||
handleDeleteClick: (feedback) => setDeleting(feedback),
|
handleDeleteClick: (feedback) => setDeleting(feedback),
|
||||||
handleStatusChange,
|
|
||||||
statuses,
|
|
||||||
canUpdateStatus,
|
|
||||||
canUpdate,
|
|
||||||
canDelete,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -143,18 +112,22 @@ export default function FeedbackIndex({
|
|||||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Kritik dan Saran"
|
title="Kritik dan Saran"
|
||||||
|
description={
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
Sampaikan kritik, saran, atau aduan Anda kepada
|
||||||
|
kami.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
actions={
|
actions={
|
||||||
canCreate && (
|
<Button asChild>
|
||||||
<Button asChild>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => setCreateOpen(true)}
|
||||||
onClick={() => setCreateOpen(true)}
|
>
|
||||||
>
|
<Plus className="h-4 w-4" />
|
||||||
<Plus className="h-4 w-4" />
|
Tambah
|
||||||
Tambah
|
</button>
|
||||||
</button>
|
</Button>
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -176,16 +149,6 @@ export default function FeedbackIndex({
|
|||||||
types={types}
|
types={types}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ViewDetailDialog
|
|
||||||
open={viewing !== null}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setViewing(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
feedback={viewing}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={feedbacks.data}
|
data={feedbacks.data}
|
||||||
@ -299,7 +262,6 @@ function CreateForm({
|
|||||||
action={store()}
|
action={store()}
|
||||||
resetOnSuccess
|
resetOnSuccess
|
||||||
onSuccess={() => onOpenChange(false)}
|
onSuccess={() => onOpenChange(false)}
|
||||||
contentClassName="sm:max-w-4xl"
|
|
||||||
>
|
>
|
||||||
{({ errors }) => (
|
{({ errors }) => (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
@ -329,7 +291,6 @@ function EditForm({
|
|||||||
action={editing ? update(editing.id) : ''}
|
action={editing ? update(editing.id) : ''}
|
||||||
resetOnSuccess
|
resetOnSuccess
|
||||||
onSuccess={() => onOpenChange(false)}
|
onSuccess={() => onOpenChange(false)}
|
||||||
contentClassName="sm:max-w-2xl"
|
|
||||||
>
|
>
|
||||||
{({ errors }) =>
|
{({ errors }) =>
|
||||||
editing && (
|
editing && (
|
||||||
@ -345,87 +306,3 @@ function EditForm({
|
|||||||
</FormDialog>
|
</FormDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
|
||||||
<DialogTitle>Detail Kritik dan Saran</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{feedback && (
|
|
||||||
<div className="grid gap-4 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">
|
|
||||||
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="rounded-md border p-3 text-sm [&_a]:text-primary [&_a]:underline [&_blockquote]:border-l-2 [&_blockquote]:pl-3 [&_blockquote]:italic [&_h2]:text-base [&_h2]:font-semibold [&_h3]:font-semibold [&_ol]:list-decimal [&_ol]:pl-5 [&_p]:mb-2 last:[&_p]:mb-0 [&_ul]:list-disc [&_ul]:pl-5"
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,17 +3,7 @@ import { Key, Pencil, Trash2 } from 'lucide-react';
|
|||||||
import { ActiveStatusSwitch } from '@/components/active-status-switch';
|
import { ActiveStatusSwitch } from '@/components/active-status-switch';
|
||||||
import { GenderBadge } from '@/components/gender-badge';
|
import { GenderBadge } from '@/components/gender-badge';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { StatusBadge } from '@/components/status-badge';
|
import { StudentStatusBadge } from '@/components/student-status-badge';
|
||||||
|
|
||||||
const StudentStatusVariants: Record<
|
|
||||||
string,
|
|
||||||
'default' | 'secondary' | 'destructive' | 'outline'
|
|
||||||
> = {
|
|
||||||
active: 'default',
|
|
||||||
on_leave: 'secondary',
|
|
||||||
graduated: 'outline',
|
|
||||||
dropped_out: 'destructive',
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Student = {
|
export type Student = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -107,10 +97,9 @@ export function createStudentColumns(
|
|||||||
accessorKey: 'student.status',
|
accessorKey: 'student.status',
|
||||||
header: () => <span>Status</span>,
|
header: () => <span>Status</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<StatusBadge
|
<StudentStatusBadge
|
||||||
status={row.original.student?.status}
|
status={row.original.student?.status}
|
||||||
statuses={statuses}
|
statuses={statuses}
|
||||||
variants={StudentStatusVariants}
|
|
||||||
onChange={(status) =>
|
onChange={(status) =>
|
||||||
handleStatusChange(row.original, status)
|
handleStatusChange(row.original, status)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,11 +18,6 @@ export const FeedbackStatusLabels: Record<FeedbackStatusValue, string> = {
|
|||||||
resolved: 'Selesai',
|
resolved: 'Selesai',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FeedbackHandler = {
|
|
||||||
id: number;
|
|
||||||
profile: { full_name: string } | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Feedback = {
|
export type Feedback = {
|
||||||
id: number;
|
id: number;
|
||||||
type: FeedbackTypeValue;
|
type: FeedbackTypeValue;
|
||||||
@ -30,8 +25,6 @@ export type Feedback = {
|
|||||||
message: string;
|
message: string;
|
||||||
status: FeedbackStatusValue;
|
status: FeedbackStatusValue;
|
||||||
admin_notes: string | null;
|
admin_notes: string | null;
|
||||||
handled_by: number | null;
|
|
||||||
handler: FeedbackHandler | null;
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|||||||
1
resources/js/types/global.d.ts
vendored
1
resources/js/types/global.d.ts
vendored
@ -14,7 +14,6 @@ declare module '@inertiajs/core' {
|
|||||||
auth: Auth;
|
auth: Auth;
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
unreadNotificationsCount: number;
|
unreadNotificationsCount: number;
|
||||||
pendingFeedbackCount: number;
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -202,12 +202,7 @@
|
|||||||
|
|
||||||
Route::resource('admin/feedback', FeedbackController::class)
|
Route::resource('admin/feedback', FeedbackController::class)
|
||||||
->except(['create', 'edit', 'show'])
|
->except(['create', 'edit', 'show'])
|
||||||
->names('admin.feedback')
|
->names('admin.feedback');
|
||||||
->middlewareFor(['index'], 'permission:view-feedback')
|
|
||||||
->middlewareFor(['store'], 'permission:create-feedback')
|
|
||||||
->middlewareFor(['update'], 'permission:update-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::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