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 Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
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
|
||||
{
|
||||
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,
|
||||
TuitionInvoiceSeeder::class,
|
||||
TuitionPaymentSeeder::class,
|
||||
AnnouncementSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
FileCheck2,
|
||||
FileText,
|
||||
GraduationCap,
|
||||
Megaphone,
|
||||
Receipt,
|
||||
School,
|
||||
User,
|
||||
@ -35,6 +36,7 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} 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 attendancesRoute } from '@/routes/admin/manage/attendances';
|
||||
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',
|
||||
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
|
||||
|
||||
use App\Http\Controllers\Admin\Manage\AnnouncementController;
|
||||
use App\Http\Controllers\Admin\Manage\AssignmentController;
|
||||
use App\Http\Controllers\Admin\Manage\AttendanceController;
|
||||
use App\Http\Controllers\Admin\Manage\ClassEnrollmentController;
|
||||
@ -70,6 +71,8 @@
|
||||
Route::put('{payment}', [TuitionPaymentController::class, 'update'])->name('update');
|
||||
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 () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user