diff --git a/app/Http/Controllers/Admin/Manage/AnnouncementController.php b/app/Http/Controllers/Admin/Manage/AnnouncementController.php index 570bf25..e8b0ecc 100644 --- a/app/Http/Controllers/Admin/Manage/AnnouncementController.php +++ b/app/Http/Controllers/Admin/Manage/AnnouncementController.php @@ -7,7 +7,6 @@ use App\Http\Requests\PaginatedRequest; use App\Models\Announcement; use App\Services\Admin\Manage\AnnouncementService; -use App\Services\Admin\Master\DepartmentService; use Illuminate\Http\RedirectResponse; use Inertia\Inertia; use Inertia\Response; @@ -16,18 +15,15 @@ class AnnouncementController extends Controller { public function __construct( private readonly AnnouncementService $service, - private readonly DepartmentService $departmentService, ) {} public function index(PaginatedRequest $request): Response { return Inertia::render('admin/manage/announcements/index', [ 'announcements' => $this->service->paginated( + $request->user(), ...$request->validatedWithDefaults(), - departmentId: $request->validated('department_id'), ), - 'departments' => $this->departmentService->getAllForSelect(), - 'filters' => $request->only(['department_id']), ]); } diff --git a/app/Http/Requests/Admin/Manage/AnnouncementRequest.php b/app/Http/Requests/Admin/Manage/AnnouncementRequest.php index 7e7f89e..b2466a4 100644 --- a/app/Http/Requests/Admin/Manage/AnnouncementRequest.php +++ b/app/Http/Requests/Admin/Manage/AnnouncementRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests\Admin\Manage; +use App\Enums\UserRole; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -17,8 +18,10 @@ public function rules(): array return [ 'title' => ['required', 'string', 'max:200'], 'content' => ['required', 'string'], - 'department_id' => ['nullable', 'integer', Rule::exists('departments', 'id')], - 'enrollment_year' => ['nullable', 'integer', 'digits:4'], + 'target_roles' => ['required', 'array', 'min:1'], + 'target_roles.*' => [ + Rule::enum(UserRole::class)->only([UserRole::Mahasiswa, UserRole::Dosen]), + ], ]; } } diff --git a/app/Models/Announcement.php b/app/Models/Announcement.php index 643a9da..02c0648 100644 --- a/app/Models/Announcement.php +++ b/app/Models/Announcement.php @@ -13,13 +13,15 @@ class Announcement extends Model { use HasFactory, SoftDeletes; + protected function casts(): array + { + return [ + 'target_roles' => 'array', + ]; + } + public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); } - - public function department(): BelongsTo - { - return $this->belongsTo(Department::class); - } } diff --git a/app/Models/Department.php b/app/Models/Department.php index cd30c68..9de8ccf 100644 --- a/app/Models/Department.php +++ b/app/Models/Department.php @@ -28,11 +28,6 @@ public function currentLeader(): HasOne return $this->hasOne(DepartmentLeadership::class)->whereNull('ended_at'); } - public function announcements(): HasMany - { - return $this->hasMany(Announcement::class); - } - public function courses(): HasMany { return $this->hasMany(Course::class); diff --git a/app/Services/Admin/Manage/AnnouncementService.php b/app/Services/Admin/Manage/AnnouncementService.php index 5fb6c49..50f7986 100644 --- a/app/Services/Admin/Manage/AnnouncementService.php +++ b/app/Services/Admin/Manage/AnnouncementService.php @@ -2,8 +2,9 @@ namespace App\Services\Admin\Manage; +use App\Enums\UserRole; use App\Models\Announcement; -use App\Models\Student; +use App\Models\User; use App\Services\NotificationService; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -13,15 +14,15 @@ public function __construct( private readonly NotificationService $notificationService, ) {} - public function paginated(int $perPage = 25, string $search = '', ?int $departmentId = null): LengthAwarePaginator + public function paginated(User $user, int $perPage = 25, string $search = ''): LengthAwarePaginator { return Announcement::query() - ->select(['id', 'title', 'content', 'department_id', 'enrollment_year', 'created_by', 'created_at']) - ->with(['department:id,name', 'creator.profile']) + ->with(['creator.profile']) ->when($search, fn ($q) => $q->where('title', 'like', "%{$search}%")) - ->when($departmentId, fn ($q) => $q->where('department_id', $departmentId)) + ->when($user->hasRole(UserRole::Mahasiswa->value), fn ($q) => $q->whereJsonContains('target_roles', UserRole::Mahasiswa->value)) + ->when($user->hasRole(UserRole::Dosen->value), fn ($q) => $q->whereJsonContains('target_roles', UserRole::Dosen->value)) ->latest() - ->paginate($perPage); + ->paginate($perPage, ['id', 'title', 'content', 'target_roles', 'created_by', 'created_at']); } public function create(array $data): Announcement @@ -29,15 +30,13 @@ public function create(array $data): Announcement $announcement = Announcement::create([ 'title' => $data['title'], 'content' => $data['content'], - 'department_id' => $data['department_id'] ?? null, - 'enrollment_year' => $data['enrollment_year'] ?? null, + 'target_roles' => $data['target_roles'], 'created_by' => auth()->id(), ]); - $recipientUserIds = Student::query() - ->when($announcement->department_id, fn ($q, $departmentId) => $q->where('department_id', $departmentId)) - ->when($announcement->enrollment_year, fn ($q, $year) => $q->where('enrollment_year', $year)) - ->pluck('user_id'); + $recipientUserIds = User::query() + ->whereHas('roles', fn ($q) => $q->whereIn('name', $announcement->target_roles)) + ->pluck('id'); $this->notificationService->sendToUsers( $recipientUserIds, @@ -51,11 +50,11 @@ public function create(array $data): Announcement public function update(Announcement $announcement, array $data): Announcement { - $announcement->title = $data['title']; - $announcement->content = $data['content']; - $announcement->department_id = $data['department_id'] ?? null; - $announcement->enrollment_year = $data['enrollment_year'] ?? null; - $announcement->update(); + $announcement->update([ + 'title' => $data['title'], + 'content' => $data['content'], + 'target_roles' => $data['target_roles'], + ]); return $announcement; } diff --git a/database/migrations/2026_08_25_000002_create_announcements_table.php b/database/migrations/2026_08_25_000002_create_announcements_table.php index 8964695..540ec51 100644 --- a/database/migrations/2026_08_25_000002_create_announcements_table.php +++ b/database/migrations/2026_08_25_000002_create_announcements_table.php @@ -12,8 +12,7 @@ public function up(): void $table->id(); $table->string('title', 200); $table->text('content'); - $table->foreignId('department_id')->nullable()->constrained()->nullOnDelete(); - $table->integer('enrollment_year')->nullable(); + $table->json('target_roles'); $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); $table->timestamps(); $table->softDeletes(); diff --git a/database/seeders/AnnouncementSeeder.php b/database/seeders/AnnouncementSeeder.php index 1a3b6c7..83f9895 100644 --- a/database/seeders/AnnouncementSeeder.php +++ b/database/seeders/AnnouncementSeeder.php @@ -13,8 +13,7 @@ public function run(): void [ 'title' => 'Libur Idul Fitri', 'content' => 'Perkuliahan diliburkan sesuai kalender akademik', - 'department_id' => null, - 'enrollment_year' => null, + 'target_roles' => json_encode(['mahasiswa', 'dosen']), 'created_by' => 5, 'created_at' => '2026-03-01 08:00:00', 'updated_at' => '2026-03-01 08:00:00', @@ -22,8 +21,7 @@ public function run(): void [ 'title' => 'Jadwal UTS Prodi SI', 'content' => 'UTS Sistem Informasi dilaksanakan tanggal 20-25 Oktober', - 'department_id' => 1, - 'enrollment_year' => null, + 'target_roles' => json_encode(['mahasiswa']), 'created_by' => 5, 'created_at' => '2026-10-01 08:00:00', 'updated_at' => '2026-10-01 08:00:00', diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 88d5ce5..aec69ad 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -52,6 +52,7 @@ public function run(): void 'view-assignments', 'submit-assignments', 'view-own-attendances', + 'view-announcements', ...$feedbackSelfService, ], 'dosen' => [ @@ -80,6 +81,7 @@ public function run(): void 'create-letter-requests', 'update-letter-requests', 'delete-letter-requests', + 'view-announcements', ...$feedbackSelfService, ], 'staff-admin' => [ diff --git a/resources/js/pages/admin/manage/announcements/columns.tsx b/resources/js/pages/admin/manage/announcements/columns.tsx index 714f93c..770ae51 100644 --- a/resources/js/pages/admin/manage/announcements/columns.tsx +++ b/resources/js/pages/admin/manage/announcements/columns.tsx @@ -1,65 +1,61 @@ import type { ColumnDef } from '@tanstack/react-table'; import { format } from 'date-fns'; -import { Pencil, Trash2 } from 'lucide-react'; +import { Eye, Pencil, Trash2 } from 'lucide-react'; import { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; import type { Announcement } from '@/types/announcement'; +import { AnnouncementTargetRoleLabels } from '@/types/announcement'; export type { Announcement } from '@/types/announcement'; type CreateColumnsParams = { + handleView: (announcement: Announcement) => void; handleEdit: (announcement: Announcement) => void; handleDeleteClick: (announcement: Announcement) => void; canUpdate: boolean; canDelete: boolean; + showTargetRoles: boolean; }; export function createAnnouncementColumns( params: CreateColumnsParams, ): ColumnDef[] { - const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params; + const { + handleView, + handleEdit, + handleDeleteClick, + canUpdate, + canDelete, + showTargetRoles, + } = params; const columns: ColumnDef[] = [ { accessorKey: 'title', header: () => Judul, - cell: ({ row }) => { - const announcement = row.original; - - return ( -
-

{announcement.title}

-

- {announcement.content} -

-
- ); - }, + cell: ({ row }) => ( + {row.original.title} + ), }, - { - accessorKey: 'department.name', - header: () => Target, - cell: ({ row }) => { - const announcement = row.original; + ]; - if (!announcement.department && !announcement.enrollment_year) { - return Semua Mahasiswa; - } - - return ( -
- - {announcement.department?.name ?? 'Semua Jurusan'} + if (showTargetRoles) { + columns.push({ + accessorKey: 'target_roles', + header: () => Ditujukan Untuk, + cell: ({ row }) => ( +
+ {row.original.target_roles.map((role) => ( + + {AnnouncementTargetRoleLabels[role]} - {announcement.enrollment_year && ( - - Angkatan {announcement.enrollment_year} - - )} -
- ); - }, - }, + ))} +
+ ), + }); + } + + columns.push( { accessorKey: 'creator.profile.full_name', header: () => Dibuat Oleh, @@ -71,38 +67,41 @@ export function createAnnouncementColumns( cell: ({ row }) => format(new Date(row.original.created_at), 'd MMM yyyy, HH:mm'), }, - ]; + ); - if (canUpdate || canDelete) { - columns.push({ - id: 'actions', - header: () => Aksi, - meta: { - className: 'w-[100px] text-center', - headerClassName: 'w-[100px] text-center', - }, - cell: ({ row }) => ( - , - show: canUpdate, - onClick: () => handleEdit(row.original), - }, - { - label: 'Hapus', - icon: ( - - ), - show: canDelete, - onClick: () => handleDeleteClick(row.original), - }, - ]} - /> - ), - }); - } + columns.push({ + id: 'actions', + header: () => Aksi, + meta: { + className: 'w-[100px] text-center', + headerClassName: 'w-[100px] text-center', + }, + cell: ({ row }) => ( + , + onClick: () => handleView(row.original), + }, + { + label: 'Edit', + icon: , + show: canUpdate, + onClick: () => handleEdit(row.original), + }, + { + label: 'Hapus', + icon: ( + + ), + show: canDelete, + onClick: () => handleDeleteClick(row.original), + }, + ]} + /> + ), + }); return columns; } diff --git a/resources/js/pages/admin/manage/announcements/index.tsx b/resources/js/pages/admin/manage/announcements/index.tsx index 6336c25..7740040 100644 --- a/resources/js/pages/admin/manage/announcements/index.tsx +++ b/resources/js/pages/admin/manage/announcements/index.tsx @@ -1,24 +1,24 @@ import { Head, router } from '@inertiajs/react'; +import { format } from 'date-fns'; import { Plus } from 'lucide-react'; -import { useRef, useState } from 'react'; +import { useState } from 'react'; 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 { FormDialog } from '@/components/form-dialog'; import InputError from '@/components/input-error'; import { PageHeader } from '@/components/page-header'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { usePermissions } from '@/hooks/use-permissions'; import { useServerTable } from '@/hooks/use-server-table'; @@ -28,11 +28,11 @@ import { store, update, } from '@/routes/admin/manage/announcements'; -import type { - Announcement, - AnnouncementDepartment, +import type { Announcement } from '@/types/announcement'; +import { + AnnouncementTargetRoleLabels, + AnnouncementTargetRoles, } from '@/types/announcement'; -import { formatDepartmentLabel } from '@/types/department'; import { createAnnouncementColumns } from './columns'; type Props = { @@ -43,38 +43,22 @@ type Props = { per_page: number; total: number; }; - departments: AnnouncementDepartment[]; highlight?: number; - filters: { - department_id?: string; - }; }; export default function AnnouncementIndex({ announcements, - departments, highlight, - filters, }: Props) { const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); + const [viewing, setViewing] = useState(null); const { hasPermission } = usePermissions(); const canCreate = hasPermission('create-announcements'); const canUpdate = hasPermission('update-announcements'); const canDelete = hasPermission('delete-announcements'); - const filterFields: FilterField[] = [ - { - key: 'department_id', - label: 'Jurusan', - options: departments.map((department) => ({ - value: String(department.id), - label: formatDepartmentLabel(department), - })), - }, - ]; - const pagination: PaginationState = { current_page: announcements.current_page, last_page: announcements.last_page, @@ -82,17 +66,11 @@ export default function AnnouncementIndex({ total: announcements.total, }; - const { - search, - handlePageChange, - handlePerPageChange, - handleSearchChange, - applyFilters, - } = useServerTable({ - route: () => announcementIndex.url(), - pagination, - filters, - }); + const { search, handlePageChange, handlePerPageChange, handleSearchChange } = + useServerTable({ + route: () => announcementIndex.url(), + pagination, + }); function handleDelete() { if (!deleting) { @@ -105,10 +83,12 @@ export default function AnnouncementIndex({ } const columns = createAnnouncementColumns({ + handleView: (announcement) => setViewing(announcement), handleEdit: (announcement) => setEditing(announcement), handleDeleteClick: (announcement) => setDeleting(announcement), canUpdate, canDelete, + showTargetRoles: canCreate || canUpdate, }); return ( @@ -140,11 +120,7 @@ export default function AnnouncementIndex({ } /> - + + + { + if (!open) { + setViewing(null); + } + }} + announcement={viewing} /> - } /> void; + announcement: Announcement | null; +}) { + return ( + + + + Detail Pengumuman + + + {announcement && ( +
+ + Dibuat oleh{' '} + {announcement.creator?.profile?.full_name ?? '-'}{' '} + ·{' '} + {format( + new Date(announcement.created_at), + 'd MMM yyyy, HH:mm', + )} + + +
+ +
+ {announcement.target_roles.map((role) => ( + + {AnnouncementTargetRoleLabels[role]} + + ))} +
+
+ +
+ +

+ {announcement.title} +

+
+ +
+ +

+ {announcement.content} +

+
+
+ )} +
+
+ ); +} + function AnnouncementFields({ errors, editing, - departments, }: { errors: Record; editing?: Announcement; - departments: AnnouncementDepartment[]; }) { - const hiddenDeptRef = useRef(null); - const initialDeptValue = editing?.department_id - ? String(editing.department_id) - : ''; - return ( <>
@@ -237,50 +273,31 @@ function AnnouncementFields({
- - - - -
-
- - - + {AnnouncementTargetRoleLabels[role]} + +
+ ))} + + ); @@ -289,11 +306,9 @@ function AnnouncementFields({ function CreateForm({ open, onOpenChange, - departments, }: { open: boolean; onOpenChange: (open: boolean) => void; - departments: AnnouncementDepartment[]; }) { return ( {({ errors }) => (
- +
)}
@@ -320,12 +332,10 @@ function EditForm({ open, onOpenChange, editing, - departments, }: { open: boolean; onOpenChange: (open: boolean) => void; editing: Announcement | null; - departments: AnnouncementDepartment[]; }) { return ( ) diff --git a/resources/js/types/announcement.ts b/resources/js/types/announcement.ts index 83c9363..9f8d11f 100644 --- a/resources/js/types/announcement.ts +++ b/resources/js/types/announcement.ts @@ -1,8 +1,13 @@ -export type AnnouncementDepartment = { - id: number; - code: string; - name: string; - degree_level: string | null; +export const AnnouncementTargetRoles = ['mahasiswa', 'dosen'] as const; + +export type AnnouncementTargetRole = (typeof AnnouncementTargetRoles)[number]; + +export const AnnouncementTargetRoleLabels: Record< + AnnouncementTargetRole, + string +> = { + mahasiswa: 'Mahasiswa', + dosen: 'Dosen', }; export type AnnouncementCreator = { @@ -14,9 +19,7 @@ export type Announcement = { id: number; title: string; content: string; - department_id: number | null; - department: AnnouncementDepartment | null; - enrollment_year: number | null; + target_roles: AnnouncementTargetRole[]; created_by: number | null; creator: AnnouncementCreator | null; created_at: string;