Compare commits
No commits in common. "fa76c647308a37bb0813a44029d917213781d0c5" and "75178c12a88d95a10319f91aec6e9f0a43f043c3" have entirely different histories.
fa76c64730
...
75178c12a8
@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Enums;
|
|
||||||
|
|
||||||
enum Semester: string
|
|
||||||
{
|
|
||||||
case odd = 'odd';
|
|
||||||
case even = 'even';
|
|
||||||
|
|
||||||
public function label(): string
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::odd => 'Ganjil',
|
|
||||||
self::even => 'Genap',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Master;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Http\Requests\Admin\Master\AcademicTermRequest;
|
|
||||||
use App\Http\Requests\PaginatedRequest;
|
|
||||||
use App\Models\AcademicTerm;
|
|
||||||
use App\Services\Admin\Master\AcademicTermService;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Inertia\Inertia;
|
|
||||||
use Inertia\Response;
|
|
||||||
|
|
||||||
class AcademicTermController extends Controller
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly AcademicTermService $service
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
|
||||||
{
|
|
||||||
return Inertia::render('admin/master/academic-terms/index', [
|
|
||||||
'academicTerms' => $this->service->paginated(...$request->validatedWithDefaults()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(AcademicTermRequest $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$this->service->create($request->validated());
|
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil ditambahkan.']);
|
|
||||||
|
|
||||||
return to_route('admin.master.academic-terms.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(AcademicTermRequest $request, AcademicTerm $AcademicTerm): RedirectResponse
|
|
||||||
{
|
|
||||||
$this->service->update($AcademicTerm, $request->validated());
|
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil diperbarui.']);
|
|
||||||
|
|
||||||
return to_route('admin.master.academic-terms.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(AcademicTerm $AcademicTerm): RedirectResponse
|
|
||||||
{
|
|
||||||
$this->service->delete($AcademicTerm);
|
|
||||||
|
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Periode akademik berhasil dihapus.']);
|
|
||||||
|
|
||||||
return to_route('admin.master.academic-terms.index');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests\Admin\Master;
|
|
||||||
|
|
||||||
use App\Enums\Semester;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class AcademicTermRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => [
|
|
||||||
'required',
|
|
||||||
'string',
|
|
||||||
'max:20',
|
|
||||||
function ($attribute, $value, $fail) {
|
|
||||||
if (! preg_match('/^(\d{4})\/(\d{4})$/', $value, $matches)) {
|
|
||||||
$fail('Format name harus YYYY/YYYY, contoh: 2025/2026.');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$startYear = (int) $matches[1];
|
|
||||||
$endYear = (int) $matches[2];
|
|
||||||
|
|
||||||
if ($endYear !== $startYear + 1) {
|
|
||||||
$fail('Tahun kedua harus selisih 1 tahun dari tahun pertama. Contoh: 2025/2026, 2026/2027.');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'semester' => [
|
|
||||||
'required',
|
|
||||||
'string',
|
|
||||||
Rule::in(array_values(Semester::cases())),
|
|
||||||
Rule::unique('academic_terms')->where(function ($query) {
|
|
||||||
return $query->where('name', $this->input('name'));
|
|
||||||
})->ignore($this->route('academic_term')),
|
|
||||||
],
|
|
||||||
'start_date' => [
|
|
||||||
'required',
|
|
||||||
'date',
|
|
||||||
function ($attribute, $value, $fail) {
|
|
||||||
$name = $this->input('name');
|
|
||||||
$semester = $this->input('semester');
|
|
||||||
if ($name && preg_match('/^(\d{4})\/(\d{4})$/', $name, $matches)) {
|
|
||||||
$startYear = (int) $matches[1];
|
|
||||||
$endYear = (int) $matches[2];
|
|
||||||
$dateYear = (int) date('Y', strtotime($value));
|
|
||||||
$expectedYear = $semester === 'even' ? $endYear : $startYear;
|
|
||||||
if ($dateYear !== $expectedYear) {
|
|
||||||
$fail('Start date harus berada di tahun '.$expectedYear.' untuk semester '.($semester === 'even' ? 'Genap' : 'Ganjil').'.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'end_date' => [
|
|
||||||
'required',
|
|
||||||
'date',
|
|
||||||
'after_or_equal:start_date',
|
|
||||||
function ($attribute, $value, $fail) {
|
|
||||||
$name = $this->input('name');
|
|
||||||
$semester = $this->input('semester');
|
|
||||||
if ($name && preg_match('/^(\d{4})\/(\d{4})$/', $name, $matches)) {
|
|
||||||
$startYear = (int) $matches[1];
|
|
||||||
$endYear = (int) $matches[2];
|
|
||||||
$dateYear = (int) date('Y', strtotime($value));
|
|
||||||
$expectedYear = $semester === 'even' ? $endYear : $startYear;
|
|
||||||
if ($dateYear !== $expectedYear) {
|
|
||||||
$fail('End date harus berada di tahun '.$expectedYear.' untuk semester '.($semester === 'even' ? 'Genap' : 'Ganjil').'.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'is_active' => ['boolean'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class PaginatedRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'per_page' => ['nullable', 'integer', 'in:25,50,100,999999'],
|
|
||||||
'search' => ['nullable', 'string', 'max:255'],
|
|
||||||
'sort' => ['nullable', 'string'],
|
|
||||||
'direction' => ['nullable', 'string', 'in:asc,desc'],
|
|
||||||
'highlight' => ['nullable', 'integer'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function validatedWithDefaults(): array
|
|
||||||
{
|
|
||||||
$validated = $this->validated();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'perPage' => $validated['per_page'] ?? 25,
|
|
||||||
'search' => $validated['search'] ?? '',
|
|
||||||
'sort' => $validated['sort'] ?? 'created_at',
|
|
||||||
'direction' => $validated['direction'] ?? 'desc',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use App\Enums\Semester;
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
#[Guarded(['id'])]
|
|
||||||
#[Appends(['formatted_start_date', 'formatted_end_date'])]
|
|
||||||
class AcademicTerm extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected function casts(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'semester' => Semester::class,
|
|
||||||
'start_date' => 'date',
|
|
||||||
'end_date' => 'date',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function formattedStartDate(): Attribute
|
|
||||||
{
|
|
||||||
return Attribute::make(
|
|
||||||
get: fn () => $this->start_date->format('d M Y'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function formattedEndDate(): Attribute
|
|
||||||
{
|
|
||||||
return Attribute::make(
|
|
||||||
get: fn () => $this->end_date->format('d M Y'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services\Admin\Master;
|
|
||||||
|
|
||||||
use App\Models\AcademicTerm;
|
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
|
|
||||||
class AcademicTermService
|
|
||||||
{
|
|
||||||
public function getAll(array $filters = []): Collection
|
|
||||||
{
|
|
||||||
return AcademicTerm::select(['id', 'name', 'semester', 'start_date', 'end_date', 'is_active'])->latest()->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
|
||||||
{
|
|
||||||
return AcademicTerm::query()
|
|
||||||
->select(['id', 'name', 'semester', 'start_date', 'end_date', 'is_active'])
|
|
||||||
->when($search, fn($q) => $q->where('name', 'like', "%{$search}%"))
|
|
||||||
->orderBy($sort, $direction)
|
|
||||||
->paginate($perPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(array $data): AcademicTerm
|
|
||||||
{
|
|
||||||
return AcademicTerm::create($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(AcademicTerm $academicTerm, array $data): AcademicTerm
|
|
||||||
{
|
|
||||||
$academicTerm->name = $data['name'];
|
|
||||||
$academicTerm->semester = $data['semester'];
|
|
||||||
$academicTerm->start_date = $data['start_date'];
|
|
||||||
$academicTerm->end_date = $data['end_date'];
|
|
||||||
$academicTerm->is_active = $data['is_active'];
|
|
||||||
$academicTerm->update();
|
|
||||||
|
|
||||||
return $academicTerm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(AcademicTerm $academicTerm): bool
|
|
||||||
{
|
|
||||||
return $academicTerm->delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Enums\Semester;
|
|
||||||
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('academic_terms', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name', 20);
|
|
||||||
$table->enum('semester', array_values(Semester::cases()));
|
|
||||||
$table->date('start_date');
|
|
||||||
$table->date('end_date');
|
|
||||||
$table->boolean('is_active')->default(false);
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('academic_terms');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Seeders;
|
|
||||||
|
|
||||||
use App\Enums\Semester;
|
|
||||||
use App\Models\AcademicTerm;
|
|
||||||
use Illuminate\Database\Seeder;
|
|
||||||
|
|
||||||
class AcademicTermSeeder extends Seeder
|
|
||||||
{
|
|
||||||
public function run(): void
|
|
||||||
{
|
|
||||||
AcademicTerm::insert([
|
|
||||||
[
|
|
||||||
'name' => '2026/2027',
|
|
||||||
'semester' => Semester::odd->value,
|
|
||||||
'start_date' => '2026-08-01',
|
|
||||||
'end_date' => '2027-01-15',
|
|
||||||
'is_active' => false,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => '2026/2027',
|
|
||||||
'semester' => Semester::even->value,
|
|
||||||
'start_date' => '2027-02-01',
|
|
||||||
'end_date' => '2027-07-15',
|
|
||||||
'is_active' => true,
|
|
||||||
'created_at' => now(),
|
|
||||||
'updated_at' => now(),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,7 +13,6 @@ public function run(): void
|
|||||||
{
|
{
|
||||||
$this->call([
|
$this->call([
|
||||||
UserSeeder::class,
|
UserSeeder::class,
|
||||||
AcademicTermSeeder::class,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
import { usePage } from "@inertiajs/react";
|
|
||||||
import type { Icon } from "@tabler/icons-react";
|
|
||||||
import {
|
import {
|
||||||
IconGridDots,
|
IconCamera,
|
||||||
|
IconChartBar,
|
||||||
|
IconDashboard,
|
||||||
|
IconFileAi,
|
||||||
|
IconFileDescription,
|
||||||
|
IconFolder,
|
||||||
IconHelp,
|
IconHelp,
|
||||||
IconMessageDots
|
IconListDetails,
|
||||||
|
IconMessageDots,
|
||||||
|
IconUsers
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
|
import { usePage } from "@inertiajs/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import AppLogoIcon from "@/components/app-logo-icon";
|
import AppLogoIcon from "@/components/app-logo-icon";
|
||||||
import { NavMain, type NavGroup, type NavItem } from "@/components/nav-main";
|
import { NavMain } from "@/components/nav-main";
|
||||||
import { NavSecondary } from "@/components/nav-secondary";
|
import { NavSecondary } from "@/components/nav-secondary";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@ -18,23 +24,79 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { index as academicTerm } from "@/routes/admin/master/academic-terms";
|
|
||||||
import { Calendar } from "lucide-react";
|
|
||||||
|
|
||||||
const data: { navMain: (NavGroup | NavItem)[]; navSecondary: { title: string; url: string; icon: Icon }[] } = {
|
const data = {
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
name: "Dasbor",
|
title: "Dashboard",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: IconGridDots,
|
icon: IconDashboard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Master",
|
title: "Lifecycle",
|
||||||
|
url: "#",
|
||||||
|
icon: IconListDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Analytics",
|
||||||
|
url: "#",
|
||||||
|
icon: IconChartBar,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Projects",
|
||||||
|
url: "#",
|
||||||
|
icon: IconFolder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Team",
|
||||||
|
url: "#",
|
||||||
|
icon: IconUsers,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
navClouds: [
|
||||||
|
{
|
||||||
|
title: "Capture",
|
||||||
|
icon: IconCamera,
|
||||||
|
isActive: true,
|
||||||
|
url: "#",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: "Periode Akademik",
|
title: "Active Proposals",
|
||||||
url: academicTerm.url(),
|
url: "#",
|
||||||
icon: Calendar,
|
},
|
||||||
|
{
|
||||||
|
title: "Archived",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Proposal",
|
||||||
|
icon: IconFileDescription,
|
||||||
|
url: "#",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Active Proposals",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Archived",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Prompts",
|
||||||
|
icon: IconFileAi,
|
||||||
|
url: "#",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Active Proposals",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Archived",
|
||||||
|
url: "#",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
|
|
||||||
type ConfirmDialogProps = {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
confirmLabel?: string;
|
|
||||||
cancelLabel?: string;
|
|
||||||
variant?: 'default' | 'destructive';
|
|
||||||
onConfirm: () => void;
|
|
||||||
loading?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ConfirmDialog({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
confirmLabel = 'Konfirmasi',
|
|
||||||
cancelLabel = 'Batal',
|
|
||||||
variant = 'destructive',
|
|
||||||
onConfirm,
|
|
||||||
loading = false,
|
|
||||||
}: ConfirmDialogProps) {
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{title}</DialogTitle>
|
|
||||||
<DialogDescription>{description}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
>
|
|
||||||
{cancelLabel}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={variant}
|
|
||||||
onClick={onConfirm}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{confirmLabel}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,68 +1,36 @@
|
|||||||
"use client"
|
import { type Icon } from "@tabler/icons-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupContent,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export type NavItem = {
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
icon: React.ComponentType<{ className?: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NavGroup = {
|
|
||||||
label: string
|
|
||||||
items: NavItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NavMain({
|
export function NavMain({
|
||||||
items,
|
items,
|
||||||
}: {
|
}: {
|
||||||
items: (NavGroup | NavItem)[]
|
items: {
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
icon?: Icon
|
||||||
|
}[]
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SidebarGroup>
|
||||||
{items.map((item, index) => {
|
<SidebarGroupContent className="flex flex-col gap-2">
|
||||||
if ("label" in item) {
|
<SidebarMenu>
|
||||||
return (
|
{items.map((item) => (
|
||||||
<SidebarGroup key={item.label} className="group-data-[collapsible=icon]:hidden">
|
<SidebarMenuItem key={item.title}>
|
||||||
<SidebarGroupLabel>{item.label}</SidebarGroupLabel>
|
<SidebarMenuButton tooltip={item.title}>
|
||||||
<SidebarMenu>
|
{item.icon && <item.icon />}
|
||||||
{item.items.map((child) => (
|
<span>{item.title}</span>
|
||||||
<SidebarMenuItem key={child.name}>
|
</SidebarMenuButton>
|
||||||
<SidebarMenuButton asChild>
|
</SidebarMenuItem>
|
||||||
<a href={child.url}>
|
))}
|
||||||
<child.icon className="h-4 w-4" />
|
</SidebarMenu>
|
||||||
<span>{child.name}</span>
|
</SidebarGroupContent>
|
||||||
</a>
|
</SidebarGroup>
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarGroup key={item.name} className="group-data-[collapsible=icon]:hidden">
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild>
|
|
||||||
<a href={item.url}>
|
|
||||||
<item.icon className="h-4 w-4" />
|
|
||||||
<span>{item.name}</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,101 +0,0 @@
|
|||||||
import { RowActions } from '@/components/row-actions';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import type { AcademicTerm } from '@/types/academic-term';
|
|
||||||
import { SemesterLabels, type Semester } from '@/types/academic-term';
|
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { Pencil, Trash2 } from 'lucide-react';
|
|
||||||
|
|
||||||
export type { AcademicTerm } from '@/types/academic-term';
|
|
||||||
|
|
||||||
type CreateColumnsParams = {
|
|
||||||
handleEdit: (academicTerm: AcademicTerm) => void;
|
|
||||||
handleDeleteClick: (academicTerm: AcademicTerm) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createAcademicTermColumns(
|
|
||||||
params: CreateColumnsParams,
|
|
||||||
): ColumnDef<AcademicTerm>[] {
|
|
||||||
const { handleEdit, handleDeleteClick } = params;
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
accessorKey: 'name',
|
|
||||||
header: () => <span>Nama</span>,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span className="font-medium">
|
|
||||||
{row.getValue('name') as string}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'semester',
|
|
||||||
header: () => <span>Semester</span>,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const semester = row.getValue('semester') as Semester;
|
|
||||||
return <span>{SemesterLabels[semester]}</span>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'formatted_start_date',
|
|
||||||
header: () => <span>Tanggal Mulai</span>,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span>{row.getValue('formatted_start_date') as string | null}</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'formatted_end_date',
|
|
||||||
header: () => <span>Tanggal Selesai</span>,
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span>{row.getValue('formatted_end_date') as string | null}</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'is_active',
|
|
||||||
header: () => <span className="block text-center">Status</span>,
|
|
||||||
meta: {
|
|
||||||
className: 'w-[100px] text-center',
|
|
||||||
headerClassName: 'w-[100px] text-center',
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const isActive = row.getValue('is_active') as boolean;
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<Badge variant={isActive ? 'default' : 'secondary'}>
|
|
||||||
{isActive ? 'Aktif' : 'Nonaktif'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
|
||||||
meta: {
|
|
||||||
className: 'w-[100px] text-center',
|
|
||||||
headerClassName: 'w-[100px] text-center',
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const academicTerm = row.original;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RowActions
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
label: 'Edit',
|
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
|
||||||
onClick: () => handleEdit(academicTerm),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Hapus',
|
|
||||||
icon: (
|
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
|
||||||
),
|
|
||||||
onClick: () => handleDeleteClick(academicTerm),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@ -1,357 +0,0 @@
|
|||||||
import { Head, router } from '@inertiajs/react';
|
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import type { PaginationState } from '@/components/data-table';
|
|
||||||
import { DataTable } from '@/components/data-table';
|
|
||||||
import { DatePicker } from '@/components/date-picker';
|
|
||||||
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 { Checkbox } from '@/components/ui/checkbox';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
|
||||||
import {
|
|
||||||
index as academicTermIndex,
|
|
||||||
destroy,
|
|
||||||
store,
|
|
||||||
update,
|
|
||||||
} from '@/routes/admin/master/academic-terms';
|
|
||||||
import type { AcademicTerm } from '@/types/academic-term';
|
|
||||||
import { Semester, SemesterLabels } from '@/types/academic-term';
|
|
||||||
import { createAcademicTermColumns } from './columns';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
academicTerms: {
|
|
||||||
data: AcademicTerm[];
|
|
||||||
current_page: number;
|
|
||||||
last_page: number;
|
|
||||||
per_page: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
highlight?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AcademicTermIndex({ academicTerms, highlight }: Props) {
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
|
||||||
const [editing, setEditing] = useState<AcademicTerm | null>(null);
|
|
||||||
const [deleting, setDeleting] = useState<AcademicTerm | null>(null);
|
|
||||||
|
|
||||||
const pagination: PaginationState = {
|
|
||||||
current_page: academicTerms.current_page,
|
|
||||||
last_page: academicTerms.last_page,
|
|
||||||
per_page: academicTerms.per_page,
|
|
||||||
total: academicTerms.total,
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
search,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
handleSearchChange,
|
|
||||||
} = useServerTable({
|
|
||||||
route: () => academicTermIndex.url(),
|
|
||||||
pagination,
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleDelete() {
|
|
||||||
if (!deleting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.delete(destroy(deleting.id), {
|
|
||||||
onSuccess: () => setDeleting(null),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = createAcademicTermColumns({
|
|
||||||
handleEdit: (academicTerm) => setEditing(academicTerm),
|
|
||||||
handleDeleteClick: (academicTerm) => setDeleting(academicTerm),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head title="Periode Akademik" />
|
|
||||||
|
|
||||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
|
||||||
<PageHeader
|
|
||||||
title="Periode Akademik"
|
|
||||||
description={
|
|
||||||
highlight && (
|
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
|
||||||
Menampilkan periode akademik dari notifikasi.
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
router.get(
|
|
||||||
academicTermIndex.url(),
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
replace: true,
|
|
||||||
preserveState: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
|
|
||||||
>
|
|
||||||
Tampilkan semua
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<Button asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setCreateOpen(true)}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Tambah
|
|
||||||
</button>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CreateForm
|
|
||||||
open={createOpen}
|
|
||||||
onOpenChange={setCreateOpen}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditForm
|
|
||||||
key={editing?.id}
|
|
||||||
open={editing !== null}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setEditing(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
editing={editing}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
columns={columns}
|
|
||||||
data={academicTerms.data}
|
|
||||||
searchKey="name"
|
|
||||||
searchPlaceholder="Cari periode akademik..."
|
|
||||||
emptyText="Belum ada data periode akademik."
|
|
||||||
pagination={pagination}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPerPageChange={handlePerPageChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
|
||||||
searchValue={search}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteConfirmDialog
|
|
||||||
target={deleting}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setDeleting(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title="Hapus Periode Akademik"
|
|
||||||
description={(academicTerm) =>
|
|
||||||
`Apakah Anda yakin ingin menghapus periode akademik "${academicTerm.name}"? Tindakan ini tidak dapat dibatalkan.`
|
|
||||||
}
|
|
||||||
onConfirm={handleDelete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateForm({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const [startDate, setStartDate] = useState<Date | undefined>();
|
|
||||||
const [endDate, setEndDate] = useState<Date | undefined>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormDialog
|
|
||||||
open={open}
|
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
title="Tambah Periode Akademik"
|
|
||||||
action={store()}
|
|
||||||
resetOnSuccess
|
|
||||||
onSuccess={() => {
|
|
||||||
onOpenChange(false);
|
|
||||||
setStartDate(undefined);
|
|
||||||
setEndDate(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ errors }) => (
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="name">
|
|
||||||
Nama <span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
placeholder="Masukkan nama periode akademik"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.name} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>
|
|
||||||
Semester <span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<RadioGroup
|
|
||||||
name="semester"
|
|
||||||
defaultValue=""
|
|
||||||
className="flex flex-row gap-4"
|
|
||||||
>
|
|
||||||
{Object.values(Semester).map((value) => (
|
|
||||||
<div key={value} className="flex items-center gap-2">
|
|
||||||
<RadioGroupItem value={value} id={`create-${value}`} />
|
|
||||||
<Label htmlFor={`create-${value}`} className="font-normal">
|
|
||||||
{SemesterLabels[value]}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
<InputError message={errors.semester} />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>
|
|
||||||
Tanggal Mulai <span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<input type="hidden" name="start_date" value={startDate ? startDate.toISOString().split('T')[0] : ''} />
|
|
||||||
<DatePicker
|
|
||||||
value={startDate}
|
|
||||||
onChange={setStartDate}
|
|
||||||
placeholder="Pilih tanggal mulai"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.start_date} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Tanggal Selesai <span className="text-destructive">*</span></Label>
|
|
||||||
<input type="hidden" name="end_date" value={endDate ? endDate.toISOString().split('T')[0] : ''} />
|
|
||||||
<DatePicker
|
|
||||||
value={endDate}
|
|
||||||
onChange={setEndDate}
|
|
||||||
placeholder="Pilih tanggal selesai"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.end_date} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input type="hidden" name="is_active" value="0" />
|
|
||||||
<Checkbox id="is_active" name="is_active" value="1" />
|
|
||||||
<Label htmlFor="is_active">Aktif</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FormDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditForm({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
editing,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
editing: AcademicTerm | null;
|
|
||||||
}) {
|
|
||||||
const [startDate, setStartDate] = useState<Date | undefined>(
|
|
||||||
editing?.start_date ? new Date(editing.start_date) : undefined,
|
|
||||||
);
|
|
||||||
const [endDate, setEndDate] = useState<Date | undefined>(
|
|
||||||
editing?.end_date ? new Date(editing.end_date) : undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormDialog
|
|
||||||
open={open}
|
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
title="Edit Periode Akademik"
|
|
||||||
action={editing ? update(editing.id) : ''}
|
|
||||||
resetOnSuccess
|
|
||||||
onSuccess={() => {
|
|
||||||
onOpenChange(false);
|
|
||||||
setStartDate(undefined);
|
|
||||||
setEndDate(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ errors }) =>
|
|
||||||
editing && (
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="edit-name">
|
|
||||||
Nama{' '}
|
|
||||||
<span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-name"
|
|
||||||
name="name"
|
|
||||||
placeholder="Masukkan nama periode akademik"
|
|
||||||
defaultValue={editing.name}
|
|
||||||
/>
|
|
||||||
<InputError message={errors.name} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>
|
|
||||||
Semester <span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<RadioGroup
|
|
||||||
name="semester"
|
|
||||||
defaultValue={editing.semester}
|
|
||||||
className="flex flex-row gap-4"
|
|
||||||
>
|
|
||||||
{Object.values(Semester).map((value) => (
|
|
||||||
<div key={value} className="flex items-center gap-2">
|
|
||||||
<RadioGroupItem value={value} id={`edit-${value}`} />
|
|
||||||
<Label htmlFor={`edit-${value}`} className="font-normal">
|
|
||||||
{SemesterLabels[value]}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
<InputError message={errors.semester} />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Tanggal Mulai <span className="text-destructive">*</span></Label>
|
|
||||||
<input type="hidden" name="start_date" value={startDate ? startDate.toISOString().split('T')[0] : ''} />
|
|
||||||
<DatePicker
|
|
||||||
value={startDate}
|
|
||||||
onChange={setStartDate}
|
|
||||||
placeholder="Pilih tanggal mulai"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.start_date} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Tanggal Selesai <span className="text-destructive">*</span></Label>
|
|
||||||
<input type="hidden" name="end_date" value={endDate ? endDate.toISOString().split('T')[0] : ''} />
|
|
||||||
<DatePicker
|
|
||||||
value={endDate}
|
|
||||||
onChange={setEndDate}
|
|
||||||
placeholder="Pilih tanggal selesai"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.end_date} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input type="hidden" name="is_active" value="0" />
|
|
||||||
<Checkbox
|
|
||||||
id="edit-is_active"
|
|
||||||
name="is_active"
|
|
||||||
value="1"
|
|
||||||
defaultChecked={editing.is_active}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="edit-is_active">Aktif</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</FormDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,36 +1,21 @@
|
|||||||
import { AppSidebar } from "@/components/app-sidebar";
|
|
||||||
import { ChartAreaInteractive } from "@/components/chart-area-interactive";
|
import { ChartAreaInteractive } from "@/components/chart-area-interactive";
|
||||||
|
import { DataTable } from "@/components/data-table";
|
||||||
import { SectionCards } from "@/components/section-cards";
|
import { SectionCards } from "@/components/section-cards";
|
||||||
import { SiteHeader } from "@/components/site-header";
|
|
||||||
import {
|
import data from "./data/data.json";
|
||||||
SidebarInset,
|
|
||||||
SidebarProvider,
|
|
||||||
} from "@/components/ui/sidebar";
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<div className="flex flex-1 flex-col">
|
||||||
style={
|
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||||
{
|
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
||||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
<SectionCards />
|
||||||
"--header-height": "calc(var(--spacing) * 12)",
|
<div className="px-4 lg:px-6">
|
||||||
} as React.CSSProperties
|
<ChartAreaInteractive />
|
||||||
}
|
|
||||||
>
|
|
||||||
<AppSidebar variant="inset" />
|
|
||||||
<SidebarInset>
|
|
||||||
<SiteHeader />
|
|
||||||
<div className="flex flex-1 flex-col">
|
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
|
||||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
|
||||||
<SectionCards />
|
|
||||||
<div className="px-4 lg:px-6">
|
|
||||||
<ChartAreaInteractive />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<DataTable data={data} />
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</div>
|
||||||
</SidebarProvider>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
export enum Semester {
|
|
||||||
Odd = 'odd',
|
|
||||||
Even = 'even',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SemesterLabels: Record<Semester, string> = {
|
|
||||||
[Semester.Odd]: 'Ganjil',
|
|
||||||
[Semester.Even]: 'Genap',
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AcademicTerm = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
semester: Semester;
|
|
||||||
start_date: string;
|
|
||||||
formatted_start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
formatted_end_date: string;
|
|
||||||
is_active: boolean;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
};
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
export type * from './academic-term';
|
|
||||||
export type * from './auth';
|
export type * from './auth';
|
||||||
export type * from './navigation';
|
export type * from './navigation';
|
||||||
export type * from './ui';
|
export type * from './ui';
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Http\Controllers\Admin\Master\AcademicTermController;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
|
|
||||||
Route::middleware(['auth', 'verified'])->prefix('admin/master')->name('admin.master.')->group(function () {
|
|
||||||
Route::resource('academic-terms', AcademicTermController::class)->except(['create', 'edit', 'show']);
|
|
||||||
});
|
|
||||||
@ -9,4 +9,3 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__.'/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
require __DIR__.'/admin.php';
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user