feat: update Announcement management to support target roles; refactor Announcement model, service, and controller; enhance UI for role-based visibility
This commit is contained in:
parent
37ce3c15ea
commit
900c27041a
@ -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']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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]),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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' => [
|
||||
|
||||
@ -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<Announcement>[] {
|
||||
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
|
||||
const {
|
||||
handleView,
|
||||
handleEdit,
|
||||
handleDeleteClick,
|
||||
canUpdate,
|
||||
canDelete,
|
||||
showTargetRoles,
|
||||
} = params;
|
||||
|
||||
const columns: ColumnDef<Announcement>[] = [
|
||||
{
|
||||
accessorKey: 'title',
|
||||
header: () => <span>Judul</span>,
|
||||
cell: ({ row }) => {
|
||||
const announcement = row.original;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="font-medium">{announcement.title}</p>
|
||||
<p className="line-clamp-1 max-w-md text-xs text-muted-foreground">
|
||||
{announcement.content}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<span className="font-medium">{row.original.title}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'department.name',
|
||||
header: () => <span>Target</span>,
|
||||
cell: ({ row }) => {
|
||||
const announcement = row.original;
|
||||
];
|
||||
|
||||
if (!announcement.department && !announcement.enrollment_year) {
|
||||
return <Badge variant="outline">Semua Mahasiswa</Badge>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="secondary">
|
||||
{announcement.department?.name ?? 'Semua Jurusan'}
|
||||
if (showTargetRoles) {
|
||||
columns.push({
|
||||
accessorKey: 'target_roles',
|
||||
header: () => <span>Ditujukan Untuk</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{row.original.target_roles.map((role) => (
|
||||
<Badge key={role} variant="secondary">
|
||||
{AnnouncementTargetRoleLabels[role]}
|
||||
</Badge>
|
||||
{announcement.enrollment_year && (
|
||||
<Badge variant="secondary">
|
||||
Angkatan {announcement.enrollment_year}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
accessorKey: 'creator.profile.full_name',
|
||||
header: () => <span>Dibuat Oleh</span>,
|
||||
@ -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: () => <span className="block text-center">Aksi</span>,
|
||||
meta: {
|
||||
className: 'w-[100px] text-center',
|
||||
headerClassName: 'w-[100px] text-center',
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<RowActions
|
||||
actions={[
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
show: canUpdate,
|
||||
onClick: () => handleEdit(row.original),
|
||||
},
|
||||
{
|
||||
label: 'Hapus',
|
||||
icon: (
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
),
|
||||
show: canDelete,
|
||||
onClick: () => handleDeleteClick(row.original),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
columns.push({
|
||||
id: 'actions',
|
||||
header: () => <span className="block text-center">Aksi</span>,
|
||||
meta: {
|
||||
className: 'w-[100px] text-center',
|
||||
headerClassName: 'w-[100px] text-center',
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<RowActions
|
||||
actions={[
|
||||
{
|
||||
label: 'Lihat Detail',
|
||||
icon: <Eye className="h-4 w-4" />,
|
||||
onClick: () => handleView(row.original),
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
show: canUpdate,
|
||||
onClick: () => handleEdit(row.original),
|
||||
},
|
||||
{
|
||||
label: 'Hapus',
|
||||
icon: (
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
),
|
||||
show: canDelete,
|
||||
onClick: () => handleDeleteClick(row.original),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
@ -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<Announcement | null>(null);
|
||||
const [deleting, setDeleting] = useState<Announcement | null>(null);
|
||||
const [viewing, setViewing] = useState<Announcement | null>(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({
|
||||
}
|
||||
/>
|
||||
|
||||
<CreateForm
|
||||
open={createOpen}
|
||||
onOpenChange={setCreateOpen}
|
||||
departments={departments}
|
||||
/>
|
||||
<CreateForm open={createOpen} onOpenChange={setCreateOpen} />
|
||||
|
||||
<EditForm
|
||||
key={editing?.id}
|
||||
@ -155,7 +131,16 @@ export default function AnnouncementIndex({
|
||||
}
|
||||
}}
|
||||
editing={editing}
|
||||
departments={departments}
|
||||
/>
|
||||
|
||||
<ViewDetailDialog
|
||||
open={viewing !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setViewing(null);
|
||||
}
|
||||
}}
|
||||
announcement={viewing}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
@ -167,13 +152,6 @@ export default function AnnouncementIndex({
|
||||
onSearchChange={handleSearchChange}
|
||||
searchValue={search}
|
||||
searchKey="title"
|
||||
toolbar={
|
||||
<FilterDialog
|
||||
fields={filterFields}
|
||||
activeFilters={filters}
|
||||
onApply={applyFilters}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
@ -194,20 +172,78 @@ export default function AnnouncementIndex({
|
||||
);
|
||||
}
|
||||
|
||||
function ViewDetailDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
announcement,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
announcement: Announcement | null;
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-lg">
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle>Detail Pengumuman</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{announcement && (
|
||||
<div className="grid min-h-0 flex-1 gap-4 overflow-x-hidden overflow-y-auto">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Dibuat oleh{' '}
|
||||
{announcement.creator?.profile?.full_name ?? '-'}{' '}
|
||||
·{' '}
|
||||
{format(
|
||||
new Date(announcement.created_at),
|
||||
'd MMM yyyy, HH:mm',
|
||||
)}
|
||||
</span>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label className="text-muted-foreground">
|
||||
Ditujukan Untuk
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{announcement.target_roles.map((role) => (
|
||||
<Badge key={role} variant="secondary">
|
||||
{AnnouncementTargetRoleLabels[role]}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label className="text-muted-foreground">
|
||||
Judul
|
||||
</Label>
|
||||
<p className="font-medium">
|
||||
{announcement.title}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label className="text-muted-foreground">
|
||||
Isi
|
||||
</Label>
|
||||
<p className="whitespace-pre-line">
|
||||
{announcement.content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function AnnouncementFields({
|
||||
errors,
|
||||
editing,
|
||||
departments,
|
||||
}: {
|
||||
errors: Record<string, string>;
|
||||
editing?: Announcement;
|
||||
departments: AnnouncementDepartment[];
|
||||
}) {
|
||||
const hiddenDeptRef = useRef<HTMLInputElement>(null);
|
||||
const initialDeptValue = editing?.department_id
|
||||
? String(editing.department_id)
|
||||
: '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
@ -237,50 +273,31 @@ function AnnouncementFields({
|
||||
<InputError message={errors.content} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Jurusan</Label>
|
||||
<input
|
||||
ref={hiddenDeptRef}
|
||||
type="hidden"
|
||||
name="department_id"
|
||||
defaultValue={initialDeptValue}
|
||||
/>
|
||||
<Select
|
||||
defaultValue={initialDeptValue || 'none'}
|
||||
onValueChange={(value) => {
|
||||
if (hiddenDeptRef.current) {
|
||||
hiddenDeptRef.current.value =
|
||||
value === 'none' ? '' : value;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Semua Jurusan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">Semua Jurusan</SelectItem>
|
||||
{departments.map((department) => (
|
||||
<SelectItem
|
||||
key={department.id}
|
||||
value={String(department.id)}
|
||||
<Label>
|
||||
Ditujukan Untuk{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{AnnouncementTargetRoles.map((role) => (
|
||||
<div key={role} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`target-role-${role}`}
|
||||
name="target_roles[]"
|
||||
value={role}
|
||||
defaultChecked={editing?.target_roles.includes(
|
||||
role,
|
||||
)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`target-role-${role}`}
|
||||
className="font-normal"
|
||||
>
|
||||
{formatDepartmentLabel(department)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.department_id} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="enrollment_year">Angkatan</Label>
|
||||
<Input
|
||||
id="enrollment_year"
|
||||
name="enrollment_year"
|
||||
type="number"
|
||||
placeholder="Kosongkan untuk semua angkatan"
|
||||
defaultValue={editing?.enrollment_year ?? ''}
|
||||
aria-invalid={!!errors.enrollment_year}
|
||||
/>
|
||||
<InputError message={errors.enrollment_year} />
|
||||
{AnnouncementTargetRoleLabels[role]}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<InputError message={errors.target_roles} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@ -289,11 +306,9 @@ function AnnouncementFields({
|
||||
function CreateForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
departments,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
departments: AnnouncementDepartment[];
|
||||
}) {
|
||||
return (
|
||||
<FormDialog
|
||||
@ -306,10 +321,7 @@ function CreateForm({
|
||||
>
|
||||
{({ errors }) => (
|
||||
<div className="grid gap-4">
|
||||
<AnnouncementFields
|
||||
errors={errors}
|
||||
departments={departments}
|
||||
/>
|
||||
<AnnouncementFields errors={errors} />
|
||||
</div>
|
||||
)}
|
||||
</FormDialog>
|
||||
@ -320,12 +332,10 @@ function EditForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
editing,
|
||||
departments,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editing: Announcement | null;
|
||||
departments: AnnouncementDepartment[];
|
||||
}) {
|
||||
return (
|
||||
<FormDialog
|
||||
@ -342,7 +352,6 @@ function EditForm({
|
||||
<AnnouncementFields
|
||||
errors={errors}
|
||||
editing={editing}
|
||||
departments={departments}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user