feat: Menambahkan controller, request, dan model Penaatan

main
marszayn 2025-03-03 11:51:49 +07:00
parent db45e28350
commit e37be6bc2d
6 changed files with 777 additions and 2 deletions

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\PenaatanRequest;
use App\Models\Penaatan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class PenaatanController extends Controller
{
public function index()
{
try {
$penaatan = Penaatan::latest()->get();
return Inertia::render('admin/penaatan/index_penaatan', ['penaatan' => $penaatan]);
} catch (\Exception $e) {
Log::error('Error fetching Status Penaatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function store(PenaatanRequest $request)
{
try {
Penaatan::create($request->validated());
return redirect()->route('admin.penataan.index')->with('success', 'Penaatan created successfully.');
} catch (\Exception $e) {
Log::error('Error creating Penaatan: ' . $e->getMessage());
return back()->with('error', 'Failed to create Penaatan.');
}
}
public function update(PenaatanRequest $request, Penaatan $penaatan)
{
try {
$penaatan->update($request->validated());
return redirect()->route('admin.penaatan.index')->with('success', 'Penaatan berhasil diperbarui.');
} catch (\Exception $e) {
Log::error('Error updating Penaatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function destroy(Penaatan $penaatan)
{
try {
$penaatan->delete();
return redirect()->route('admin.penaatan.index')->with('success', 'Penaatan berhasil dihapus.');
} catch (\Exception $e) {
Log::error('Error deleting Penaatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PenaatanRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'NamaPenaatan' => ['required', 'string', 'max:255'],
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Penaatan extends Model
{
protected $table = 'Penaatan';
protected $primaryKey = 'PenaatanId';
protected $fillable = ['NamaPenaatan'];
public function hukum()
{
return $this->hasMany(Hukum::class, 'PenaatanId');
}
}

View File

@ -0,0 +1,208 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Select from "react-select";
import { useState, useEffect } from "react";
import { useForm } from "@inertiajs/react";
import { useToast } from "@/hooks/use-toast";
import { HukumType } from "@/types/perusahaan";
interface AddPenaatanModalProps {
open: boolean;
onClose: () => void;
onSuccess: () => void;
editingData: HukumType | null;
penaatan: { PenaatanId: number; NamaPenaatan: string }[];
}
export function AddPenaatanModal({
open,
onClose,
onSuccess,
editingData,
penaatan,
}: AddPenaatanModalProps) {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const { data, setData, post, reset } = useForm<{
PenaatanId: string;
PenaatanNumber: string;
PenaatanDate: string;
PenaatanFile: File | null;
currentPenaatanFile?: string;
}>({
PenaatanId: "",
PenaatanNumber: "",
PenaatanDate: "",
PenaatanFile: null,
currentPenaatanFile: editingData?.PenaatanFile,
});
const [isPenaatanModalOpen, setIsPenaatanModalOpen] = useState(false);
const [selectedHukum, setSelectedHukum] = useState<HukumType | null>(null);
// Handle Open Modal for Penaatan
const handleOpenPenaatanModal = (hukum: HukumType) => {
setSelectedHukum(hukum);
setIsPenaatanModalOpen(true);
};
useEffect(() => {
if (editingData) {
setData({
PenaatanId: editingData.PenaatanId?.toString() || "",
PenaatanNumber: editingData.PenaatanNumber || "",
PenaatanDate: editingData.PenaatanDate || "",
PenaatanFile: null,
currentPenaatanFile: "",
});
} else {
reset();
}
}, [editingData, open]);
const penaatanOptions = penaatan.map((p) => ({
value: p.PenaatanId.toString(),
label: p.NamaPenaatan,
}));
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
const formData = new FormData();
formData.append("PenaatanId", data.PenaatanId);
formData.append("PenaatanNumber", data.PenaatanNumber);
formData.append("PenaatanDate", data.PenaatanDate);
if (data.PenaatanFile)
formData.append("PenaatanFile", data.PenaatanFile);
if (!editingData) return;
post(`/admin/hukum/${editingData.HukumId}/penaatan`, {
data: formData,
forceFormData: true,
onSuccess: () => {
toast({
title: "Berhasil",
description: "Data Penaatan berhasil diperbarui",
variant: "default",
});
reset();
setLoading(false);
onSuccess();
onClose();
},
onError: () => {
toast({
title: "Gagal",
description: "Terjadi kesalahan saat memperbarui data",
variant: "destructive",
});
setLoading(false);
},
});
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Perbarui Data Penaatan</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<Label>Status Penaatan</Label>
<Select
id="PenaatanId"
options={penaatanOptions}
placeholder="Pilih Status Penaatan"
value={penaatanOptions.find(
(p) => p.value === data.PenaatanId
)}
onChange={(option) =>
setData("PenaatanId", option?.value || "")
}
/>
<Label>Nomor SK Penaatan</Label>
<Input
value={data.PenaatanNumber}
onChange={(e) =>
setData("PenaatanNumber", e.target.value)
}
/>
<Label>Tanggal SK Penaatan</Label>
<Input
type="date"
value={data.PenaatanDate}
onChange={(e) =>
setData("PenaatanDate", e.target.value)
}
/>
<Label>Dokumen SK Penaatan</Label>
<div className="flex flex-col gap-2">
<Input
type="file"
onChange={(e) =>
setData(
"PenaatanFile",
e.target.files
? e.target.files[0]
: null
)
}
/>
{editingData?.PenaatanFile && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
File saat ini:{" "}
{editingData.PenaatanFile}
</span>
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
window.open(
`/storage/${editingData.PenaatanFile}`,
"_blank"
)
}
>
Lihat File
</Button>
</div>
)}
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={onClose}
disabled={loading}
>
Batal
</Button>
<Button type="submit" disabled={loading}>
{loading ? "Menyimpan..." : "Simpan"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@ -179,7 +179,7 @@ AddPerusahaanModalProps) {
const perusahaanOptions = perusahaan.map((per) => ({ const perusahaanOptions = perusahaan.map((per) => ({
value: per.PerusahaanId, value: per.PerusahaanId,
label: per.PerusahaanId, label: per.NamaPerusahaan,
})); }));
const kecamatanOptions = kecamatan const kecamatanOptions = kecamatan
@ -439,7 +439,7 @@ AddPerusahaanModalProps) {
onChange={(option) => onChange={(option) =>
setData({ setData({
...data, ...data,
PerusahaanId: NamaPerusahaan:
option?.value?.toString() || option?.value?.toString() ||
"", "",
}) })

View File

@ -0,0 +1,461 @@
import React, { useEffect, useState } from "react";
import { useForm } from "@inertiajs/react";
import { PageProps } from "@/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Table,
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell,
} from "@/components/ui/table";
// import { toast } from "react-toastify";
import { useToast } from "@/hooks/use-toast";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import {
Search,
Plus,
Pencil,
Trash2,
ChevronLeft,
ChevronRight,
} from "lucide-react";
import AuthenticatedLayout from "@/layouts/authenticated-layout";
import { Head } from "@inertiajs/react";
import { Toaster } from "@/components/ui/toaster";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface Penaatan {
PenaatanId: number | null;
NamaPenaatan: string;
}
const ITEMS_PER_PAGE = 5;
export default function PenaatanIndex({
penaatan,
}: PageProps<{ penaatan: Penaatan[] }>) {
const {
data,
setData,
post,
put,
delete: destroy,
reset,
} = useForm<Penaatan>({
PenaatanId: null,
NamaPenaatan: "",
});
const { toast } = useToast();
const [editing, setEditing] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<Penaatan | null>(null);
const [search, setSearch] = useState("");
const [filteredPenaatan, setFilteredPenaatan] = useState(penaatan);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
let filtered = penaatan;
if (search) {
filtered = filtered.filter((penaatan) =>
penaatan.NamaPenaatan.toLowerCase().includes(
search.toLowerCase()
)
);
}
setFilteredPenaatan(filtered);
setCurrentPage(1);
}, [penaatan, search]);
const totalPages = Math.ceil(filteredPenaatan.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const currentItems = filteredPenaatan.slice(startIndex, endIndex);
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (editing) {
put(`/admin/penaatan/${data.PenaatanId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Penaatan berhasil diperbarui",
variant: "default",
});
setIsModalOpen(false);
reset();
setEditing(false);
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat memperbarui Penaatan",
variant: "destructive",
});
},
});
} else {
post("/admin/penaatan", {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Penaatan berhasil dibuat",
variant: "default",
});
setIsModalOpen(false);
reset();
},
onError: () => {
toast({
title: "Gagal",
description: "Terjadi kesalahan saat membuat Penaatan",
variant: "destructive",
});
},
});
}
};
const handleEdit = (penaatan: Penaatan) => {
setData({ ...penaatan });
setEditing(true);
setIsModalOpen(true);
};
const handleDelete = () => {
if (deleteConfirm) {
destroy(`/admin/penaatan/${deleteConfirm.PenaatanId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Penaatan berhasil dihapus",
variant: "default",
});
setDeleteConfirm(null);
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat menghapus Penaatan",
variant: "destructive",
});
setDeleteConfirm(null);
},
});
}
};
return (
<AuthenticatedLayout header={"Status Penaatan"}>
<Head title="Status Penaatan" />
<div className="container mx-auto p-4">
<Card className="shadow-lg">
<CardHeader>
<div className="flex flex-col space-y-4">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div className="relative w-full md:w-96">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-4 w-4" />
<Input
type="text"
placeholder="Cari Penaatan..."
value={search}
onChange={handleSearch}
className="pl-10 pr-4 w-full border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 rounded-lg"
/>
</div>
<Dialog
open={isModalOpen}
onOpenChange={setIsModalOpen}
>
<DialogTrigger asChild>
<Button
onClick={() => {
setEditing(false);
setIsModalOpen(true);
}}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 rounded-lg flex items-center gap-2 transition-colors duration-200"
>
<Plus className="h-4 w-4" />
Buat Penaatan
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
{editing
? "Ubah Penaatan"
: "Buat Penaatan"}
</DialogTitle>
</DialogHeader>
<form
onSubmit={handleSubmit}
className="flex flex-col gap-4 mt-4"
>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700">
Name
</label>
<Input
type="text"
placeholder="Masukkan nama Penaatan"
value={data.NamaPenaatan}
onChange={(e) =>
setData(
"NamaPenaatan",
e.target.value
)
}
className="w-full"
/>
{/* <div className="space-y-2">
<label className="text-sm font-medium text-gray-700">
Publish
</label>
<Select
value={
data.is_publish
? "true"
: "false"
}
onValueChange={(
value
) =>
setData(
"is_publish",
value === "true"
)
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">
Published
</SelectItem>
<SelectItem value="false">
Unpublished
</SelectItem>
</SelectContent>
</Select>
</div> */}
</div>
<DialogFooter className="mt-6">
<Button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
{editing ? "Ubah" : "Buat"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</div>
</CardHeader>
<CardContent>
<div className="rounded-lg border border-gray-200 overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-gray-50 dark:bg-black/50">
{/* <TableHead className="font-semibold">
ID
</TableHead> */}
<TableHead className="font-semibold">
No
</TableHead>
<TableHead className="font-semibold">
Name
</TableHead>
<TableHead className="font-semibold">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{currentItems.map((penaatan, index) => (
<TableRow
key={penaatan.PenaatanId || "new"}
className="hover:bg-gray-50 transition-colors duration-150"
>
{/* <TableCell>{category.id}</TableCell> */}
<TableCell>
{startIndex + index + 1}
</TableCell>
<TableCell>
{penaatan.NamaPenaatan}
</TableCell>
<TableCell className="flex gap-2">
<Button
onClick={() =>
handleEdit(penaatan)
}
variant="outline"
className="flex items-center gap-2"
>
<Pencil className="h-4 w-4" />
Edit
</Button>
<Button
onClick={() =>
setDeleteConfirm(
penaatan
)
}
variant="destructive"
className="flex items-center gap-2"
>
<Trash2 className="h-4 w-4" />
Delete
</Button>
</TableCell>
</TableRow>
))}
{currentItems.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center py-4 text-gray-500"
>
Tidak ada data
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* <div className="flex flex-row mt-3">
<div className="text-sm w-1/2">
1 : Pengumuman <br /> 2 : Undangan
</div>
<div className="text-sm w-1/2">
3 : Peraturan <br /> 4 : Popup Home
</div>
</div> */}
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-gray-500">
Showing {startIndex + 1} to{" "}
{Math.min(endIndex, filteredPenaatan.length)} of{" "}
{filteredPenaatan.length} entries
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() =>
handlePageChange(currentPage - 1)
}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
{Array.from(
{ length: totalPages },
(_, i) => i + 1
).map((page) => (
<Button
key={page}
variant={
currentPage === page
? "default"
: "outline"
}
size="sm"
onClick={() => handlePageChange(page)}
>
{page}
</Button>
))}
<Button
variant="outline"
size="sm"
onClick={() =>
handlePageChange(currentPage + 1)
}
disabled={currentPage === totalPages}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
{deleteConfirm && (
<Dialog open={true} onOpenChange={() => setDeleteConfirm(null)}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
Konfirmasi Penghapusan
</DialogTitle>
</DialogHeader>
<div className="py-4">
<p className="text-gray-600">
Apakah Anda yakin ingin menghapus "
<span className="font-medium">
{deleteConfirm.NamaPenaatan}
</span>
"?
<br />
<span className="text-red-500">
Tindakan ini tidak dapat dibatalkan.
</span>
</p>
</div>
<DialogFooter className="gap-2">
<Button
onClick={() => setDeleteConfirm(null)}
variant="outline"
>
Batal
</Button>
<Button
onClick={handleDelete}
variant="destructive"
className="flex items-center gap-2"
>
<Trash2 className="h-4 w-4" />
Ya, hapus
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
<Toaster />
</AuthenticatedLayout>
);
}