feat: add announcement management functionality with CRUD operations
Some checks failed
tests / ci (pull_request) Has been cancelled
Some checks failed
tests / ci (pull_request) Has been cancelled
This commit is contained in:
parent
24cda20051
commit
e268153971
54
app/Http/Controllers/Admin/Manage/AnnouncementController.php
Normal file
54
app/Http/Controllers/Admin/Manage/AnnouncementController.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\Manage\AnnouncementRequest;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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->validatedWithDefaults()),
|
||||||
|
'departments' => $this->departmentService->getAll(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(AnnouncementRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->service->create($request->validated());
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumuman berhasil ditambahkan.']);
|
||||||
|
|
||||||
|
return to_route('admin.manage.announcements.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(AnnouncementRequest $request, Announcement $announcement): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->service->update($announcement, $request->validated());
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumuman berhasil diperbarui.']);
|
||||||
|
|
||||||
|
return to_route('admin.manage.announcements.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Announcement $announcement): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->service->delete($announcement);
|
||||||
|
|
||||||
|
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Pengumuman berhasil dihapus.'])->back();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Http/Requests/Admin/Manage/AnnouncementRequest.php
Normal file
24
app/Http/Requests/Admin/Manage/AnnouncementRequest.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin\Manage;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class AnnouncementRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Models/Announcement.php
Normal file
25
app/Models/Announcement.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
#[Guarded(['id'])]
|
||||||
|
class Announcement extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
public function department(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Department::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function creator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Services/Admin/Manage/AnnouncementService.php
Normal file
46
app/Services/Admin/Manage/AnnouncementService.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Models\Announcement;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
class AnnouncementService
|
||||||
|
{
|
||||||
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
return Announcement::query()
|
||||||
|
->select(['id', 'title', 'content', 'department_id', 'enrollment_year', 'created_by', 'created_at'])
|
||||||
|
->with(['department:id,name', 'creator.profile'])
|
||||||
|
->when($search, fn ($q) => $q->where('title', 'like', "%{$search}%"))
|
||||||
|
->orderBy($sort, $direction)
|
||||||
|
->paginate($perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(array $data): Announcement
|
||||||
|
{
|
||||||
|
return Announcement::create([
|
||||||
|
'title' => $data['title'],
|
||||||
|
'content' => $data['content'],
|
||||||
|
'department_id' => $data['department_id'] ?? null,
|
||||||
|
'enrollment_year' => $data['enrollment_year'] ?? null,
|
||||||
|
'created_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
return $announcement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Announcement $announcement): bool
|
||||||
|
{
|
||||||
|
return $announcement->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,9 +4,15 @@
|
|||||||
|
|
||||||
use App\Models\Department;
|
use App\Models\Department;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
class DepartmentService
|
class DepartmentService
|
||||||
{
|
{
|
||||||
|
public function getAll(): Collection
|
||||||
|
{
|
||||||
|
return Department::select(['id', 'code', 'name'])->get();
|
||||||
|
}
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return Department::query()
|
return Department::query()
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
<?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::create('announcements', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('title', 200);
|
||||||
|
$table->text('content');
|
||||||
|
$table->foreignId('department_id')->nullable()->constrained()->nullOnDelete();
|
||||||
|
$table->integer('enrollment_year')->nullable();
|
||||||
|
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('announcements');
|
||||||
|
}
|
||||||
|
};
|
||||||
33
database/seeders/AnnouncementSeeder.php
Normal file
33
database/seeders/AnnouncementSeeder.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Announcement;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class AnnouncementSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
Announcement::insert([
|
||||||
|
[
|
||||||
|
'title' => 'Libur Idul Fitri',
|
||||||
|
'content' => 'Perkuliahan diliburkan sesuai kalender akademik',
|
||||||
|
'department_id' => null,
|
||||||
|
'enrollment_year' => null,
|
||||||
|
'created_by' => 5,
|
||||||
|
'created_at' => '2026-03-01 08:00:00',
|
||||||
|
'updated_at' => '2026-03-01 08:00:00',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Jadwal UTS Prodi SI',
|
||||||
|
'content' => 'UTS Sistem Informasi dilaksanakan tanggal 20-25 Oktober',
|
||||||
|
'department_id' => 1,
|
||||||
|
'enrollment_year' => null,
|
||||||
|
'created_by' => 5,
|
||||||
|
'created_at' => '2026-10-01 08:00:00',
|
||||||
|
'updated_at' => '2026-10-01 08:00:00',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,6 +28,7 @@ public function run(): void
|
|||||||
AttendanceSeeder::class,
|
AttendanceSeeder::class,
|
||||||
TuitionInvoiceSeeder::class,
|
TuitionInvoiceSeeder::class,
|
||||||
TuitionPaymentSeeder::class,
|
TuitionPaymentSeeder::class,
|
||||||
|
AnnouncementSeeder::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
FileCheck2,
|
FileCheck2,
|
||||||
FileText,
|
FileText,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
|
Megaphone,
|
||||||
Receipt,
|
Receipt,
|
||||||
School,
|
School,
|
||||||
User,
|
User,
|
||||||
@ -35,6 +36,7 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
|
import { index as announcementsRoute } from '@/routes/admin/manage/announcements';
|
||||||
import { index as assignmentsRoute } from '@/routes/admin/manage/assignments';
|
import { index as assignmentsRoute } from '@/routes/admin/manage/assignments';
|
||||||
import { index as attendancesRoute } from '@/routes/admin/manage/attendances';
|
import { index as attendancesRoute } from '@/routes/admin/manage/attendances';
|
||||||
import { index as courseClassesRoute } from '@/routes/admin/manage/course-classes';
|
import { index as courseClassesRoute } from '@/routes/admin/manage/course-classes';
|
||||||
@ -129,6 +131,16 @@ const data: {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Pengumuman',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'Pengumuman',
|
||||||
|
url: announcementsRoute.url(),
|
||||||
|
icon: Megaphone,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Pengguna',
|
label: 'Pengguna',
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
99
resources/js/pages/admin/manage/announcements/columns.tsx
Normal file
99
resources/js/pages/admin/manage/announcements/columns.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
|
import { RowActions } from '@/components/row-actions';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import type { Announcement } from '@/types/announcement';
|
||||||
|
|
||||||
|
export type { Announcement } from '@/types/announcement';
|
||||||
|
|
||||||
|
type CreateColumnsParams = {
|
||||||
|
handleEdit: (announcement: Announcement) => void;
|
||||||
|
handleDeleteClick: (announcement: Announcement) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createAnnouncementColumns(
|
||||||
|
params: CreateColumnsParams,
|
||||||
|
): ColumnDef<Announcement>[] {
|
||||||
|
const { handleEdit, handleDeleteClick } = params;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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'}
|
||||||
|
</Badge>
|
||||||
|
{announcement.enrollment_year && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
Angkatan {announcement.enrollment_year}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'creator.profile.full_name',
|
||||||
|
header: () => <span>Dibuat Oleh</span>,
|
||||||
|
cell: ({ row }) => row.original.creator?.profile?.full_name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: () => <span>Tanggal</span>,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
format(new Date(row.original.created_at), 'd MMM yyyy, HH:mm'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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" />,
|
||||||
|
onClick: () => handleEdit(row.original),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hapus',
|
||||||
|
icon: (
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
),
|
||||||
|
onClick: () => handleDeleteClick(row.original),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
318
resources/js/pages/admin/manage/announcements/index.tsx
Normal file
318
resources/js/pages/admin/manage/announcements/index.tsx
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
import { Head, router } from '@inertiajs/react';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import type { PaginationState } from '@/components/data-table';
|
||||||
|
import { DataTable } from '@/components/data-table';
|
||||||
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
|
import { FormDialog } from '@/components/form-dialog';
|
||||||
|
import InputError from '@/components/input-error';
|
||||||
|
import { PageHeader } from '@/components/page-header';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
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 { useServerTable } from '@/hooks/use-server-table';
|
||||||
|
import {
|
||||||
|
index as announcementIndex,
|
||||||
|
destroy,
|
||||||
|
store,
|
||||||
|
update,
|
||||||
|
} from '@/routes/admin/manage/announcements';
|
||||||
|
import type {
|
||||||
|
Announcement,
|
||||||
|
AnnouncementDepartment,
|
||||||
|
} from '@/types/announcement';
|
||||||
|
import { createAnnouncementColumns } from './columns';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
announcements: {
|
||||||
|
data: Announcement[];
|
||||||
|
current_page: number;
|
||||||
|
last_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
departments: AnnouncementDepartment[];
|
||||||
|
highlight?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AnnouncementIndex({
|
||||||
|
announcements,
|
||||||
|
departments,
|
||||||
|
highlight,
|
||||||
|
}: Props) {
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [editing, setEditing] = useState<Announcement | null>(null);
|
||||||
|
const [deleting, setDeleting] = useState<Announcement | null>(null);
|
||||||
|
|
||||||
|
const pagination: PaginationState = {
|
||||||
|
current_page: announcements.current_page,
|
||||||
|
last_page: announcements.last_page,
|
||||||
|
per_page: announcements.per_page,
|
||||||
|
total: announcements.total,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
search,
|
||||||
|
handlePageChange,
|
||||||
|
handlePerPageChange,
|
||||||
|
handleSearchChange,
|
||||||
|
} = useServerTable({
|
||||||
|
route: () => announcementIndex.url(),
|
||||||
|
pagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
if (!deleting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.delete(destroy(deleting.id), {
|
||||||
|
onSuccess: () => setDeleting(null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = createAnnouncementColumns({
|
||||||
|
handleEdit: (announcement) => setEditing(announcement),
|
||||||
|
handleDeleteClick: (announcement) => setDeleting(announcement),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head title="Pengumuman" />
|
||||||
|
|
||||||
|
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||||
|
<PageHeader
|
||||||
|
title="Pengumuman"
|
||||||
|
description={
|
||||||
|
highlight && (
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
Menampilkan pengumuman dari notifikasi.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<Button asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCreateOpen(true)}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tambah
|
||||||
|
</button>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CreateForm
|
||||||
|
open={createOpen}
|
||||||
|
onOpenChange={setCreateOpen}
|
||||||
|
departments={departments}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditForm
|
||||||
|
key={editing?.id}
|
||||||
|
open={editing !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setEditing(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
editing={editing}
|
||||||
|
departments={departments}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={announcements.data}
|
||||||
|
emptyText="Belum ada pengumuman."
|
||||||
|
pagination={pagination}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onPerPageChange={handlePerPageChange}
|
||||||
|
onSearchChange={handleSearchChange}
|
||||||
|
searchValue={search}
|
||||||
|
searchKey="title"
|
||||||
|
searchPlaceholder="Cari judul pengumuman..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
target={deleting}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setDeleting(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Hapus Pengumuman"
|
||||||
|
description={(announcement) =>
|
||||||
|
`Apakah Anda yakin ingin menghapus pengumuman "${announcement.title}"? Tindakan ini tidak dapat dibatalkan.`
|
||||||
|
}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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">
|
||||||
|
<Label htmlFor="title">
|
||||||
|
Judul <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
name="title"
|
||||||
|
placeholder="Judul pengumuman"
|
||||||
|
defaultValue={editing?.title ?? ''}
|
||||||
|
aria-invalid={!!errors.title}
|
||||||
|
/>
|
||||||
|
<InputError message={errors.title} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="content">
|
||||||
|
Isi <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="content"
|
||||||
|
name="content"
|
||||||
|
placeholder="Isi pengumuman"
|
||||||
|
defaultValue={editing?.content ?? ''}
|
||||||
|
aria-invalid={!!errors.content}
|
||||||
|
/>
|
||||||
|
<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)}
|
||||||
|
>
|
||||||
|
{department.name}
|
||||||
|
</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} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateForm({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
departments,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
departments: AnnouncementDepartment[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FormDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title="Tambah Pengumuman"
|
||||||
|
action={store()}
|
||||||
|
resetOnSuccess
|
||||||
|
onSuccess={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{({ errors }) => (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<AnnouncementFields
|
||||||
|
errors={errors}
|
||||||
|
departments={departments}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditForm({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
editing,
|
||||||
|
departments,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
editing: Announcement | null;
|
||||||
|
departments: AnnouncementDepartment[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FormDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title="Edit Pengumuman"
|
||||||
|
action={editing ? update(editing.id) : ''}
|
||||||
|
resetOnSuccess
|
||||||
|
onSuccess={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
{({ errors }) =>
|
||||||
|
editing && (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<AnnouncementFields
|
||||||
|
errors={errors}
|
||||||
|
editing={editing}
|
||||||
|
departments={departments}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</FormDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
resources/js/types/announcement.ts
Normal file
23
resources/js/types/announcement.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export type AnnouncementDepartment = {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnnouncementCreator = {
|
||||||
|
id: number;
|
||||||
|
profile: { full_name: string } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Announcement = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
department_id: number | null;
|
||||||
|
department: AnnouncementDepartment | null;
|
||||||
|
enrollment_year: number | null;
|
||||||
|
created_by: number | null;
|
||||||
|
creator: AnnouncementCreator | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Admin\Manage\AnnouncementController;
|
||||||
use App\Http\Controllers\Admin\Manage\AssignmentController;
|
use App\Http\Controllers\Admin\Manage\AssignmentController;
|
||||||
use App\Http\Controllers\Admin\Manage\AttendanceController;
|
use App\Http\Controllers\Admin\Manage\AttendanceController;
|
||||||
use App\Http\Controllers\Admin\Manage\ClassEnrollmentController;
|
use App\Http\Controllers\Admin\Manage\ClassEnrollmentController;
|
||||||
@ -70,6 +71,8 @@
|
|||||||
Route::put('{payment}', [TuitionPaymentController::class, 'update'])->name('update');
|
Route::put('{payment}', [TuitionPaymentController::class, 'update'])->name('update');
|
||||||
Route::delete('{payment}', [TuitionPaymentController::class, 'destroy'])->name('destroy');
|
Route::delete('{payment}', [TuitionPaymentController::class, 'destroy'])->name('destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::resource('announcements', AnnouncementController::class)->except(['create', 'edit', 'show']);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('admin/users')->name('admin.users.')->group(function () {
|
Route::prefix('admin/users')->name('admin.users.')->group(function () {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user