versi presentasi

main
marszayn 2025-02-20 08:59:42 +07:00
parent f00612339c
commit 0562099ca0
111 changed files with 9387 additions and 933 deletions

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Http\Requests\RoleRequest;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class RoleController extends Controller
@ -18,4 +19,84 @@ class RoleController extends Controller
// Kembalikan data ke komponen Inertia 'Admin/Roles/Index'
return inertia('Admin/Roles/Index', compact('roles'));
}
public function create()
{
// Ambil semua permission
$permissions = Permission::all();
// Kembalikan ke Inertia 'Admin/Roles/Create' dengan data permissions
return inertia('Admin/Roles/Create', [
'permissions' => $permissions,
]);
}
public function store(RoleRequest $request)
{
// Validasi data input melalui RoleRequest
$validatedData = $request->validated();
// Buat role baru dengan data yang telah divalidasi
$role = Role::create($validatedData);
// Tetapkan permissions pada role yang baru dibuat
$role->syncPermissions($request->input('permissions'));
// Jika berhasil simpan, arahkan dengan pesan sukses, jika gagal dengan pesan error
if ($role) {
return redirect()->route('admin.roles.index')->with(['success' => 'Data Berhasil Disimpan!']);
} else {
return redirect()->route('admin.roles.index')->with(['error' => 'Data Gagal Disimpan!']);
}
}
public function edit(Role $role)
{
// Ambil semua permission, diurutkan dari yang terbaru
$permissions = Permission::latest()->get();
// Ambil ID dari permissions yang dimiliki oleh role saat ini
$rolePermissions = $role->permissions->pluck('id')->toArray();
// Map permissions agar hanya menyertakan 'id' dan 'name'
$permissions = $permissions->map(function ($permission) {
return [
'id' => $permission->id,
'name' => $permission->name,
];
});
// Kembalikan data ke Inertia 'Admin/Roles/Edit' dengan data role, permissions, dan rolePermissions
return inertia('Admin/Roles/Edit', [
'permissions' => $permissions,
'role' => $role,
'rolePermissions' => $rolePermissions,
]);
}
public function update(RoleRequest $request, Role $role)
{
// Update data role dengan data yang telah divalidasi
$role->name = $request->name;
// Sync permissions
$role->permissions()->sync($request->permissions);
// Simpan perubahan
$role->save();
// Arahkan kembali ke daftar role
return redirect()->route('admin.roles.index')->with('success', 'Role updated successfully.');
}
public function destroy($id)
{
// Cari role berdasarkan ID, error jika tidak ditemukan
$role = Role::findOrFail($id);
// Hapus role dari database
$role->delete();
// Arahkan kembali dengan pesan bahwa role berhasil dihapus
return redirect()->route('admin.roles.index')->with('message', 'Role deleted successfully.');
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\HistoryKegiatanRequest;
use App\Models\HistoryKegiatan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class HistoryKegiatanController extends Controller
{
public function index()
{
try {
$historykegiatan = HistoryKegiatan::latest()->get();
return Inertia::render('admin/kegiatan/index_history_kegiatan', ['historykegiatan' => $historykegiatan]);
} catch (\Exception $e) {
Log::error('Error fetching History Kegiatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function store(HistoryKegiatanRequest $request)
{
try {
$historykegiatan = HistoryKegiatan::withTrashed()
->where('NamaHistoryKegiatan', $request->NamaHistoryKegiatan)
->first();
if ($historykegiatan) {
$historykegiatan->restore();
return redirect()->route('admin.historykegiatan.index')->with('success', 'History Kegiatan berhasil dikembalikan.');
}
HistoryKegiatan::create($request->validated());
return redirect()->route('admin.historykegiatan.index')->with('success', 'History Kegiatan berhasil dibuat.');
} catch (\Exception $e) {
Log::error('Error creating History Kegiatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function update(HistoryKegiatanRequest $request, HistoryKegiatan $historykegiatan)
{
try {
$historykegiatan->update($request->validated());
return redirect()->route('admin.history$historykegiatan.index')->with('success', 'History Kegiatan berhasil diperbarui.');
} catch (\Exception $e) {
Log::error('Error updating History Kegiatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function destroy(HistoryKegiatan $historykegiatan)
{
try {
$historykegiatan->delete();
return redirect()->route('admin.histo$historykegiatan.index')->with('success', 'History Kegiatan berhasil dihapus.');
} catch (\Exception $e) {
Log::error('Error deleting History Kegiatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Inertia;
class HomeController extends Controller
{
public function index()
{
$runPosts = Post::with(['kategori', 'subkategori'])
->where('IsPublish', true)
->orderBy('created_at', 'desc')
->get();
// Fetch Pengumuman
$pengumuman = Post::with(['kategori', 'subkategori'])
->whereHas('kategori', function($query) {
$query->where('NamaKategori', 'Pengumuman');
})
->where('IsPublish', true)
->orderBy('created_at', 'desc')
->take(4)
->get();
// Fetch Undangan
$undangan = Post::with(['kategori', 'subkategori'])
->whereHas('kategori', function($query) {
$query->where('NamaKategori', 'Undangan');
})
->where('IsPublish', true)
->orderBy('created_at', 'desc')
->take(4)
->get();
// Fetch Peraturan
$peraturan = Post::with(['kategori', 'subkategori'])
->whereHas('kategori', function($query) {
$query->where('NamaKategori', 'Peraturan');
})
->where('IsPublish', true)
->orderBy('created_at', 'desc')
->take(4)
->get();
// Fetch Sekilas Info
$sekilasInfo = Post::with(['kategori', 'subkategori'])
->whereHas('kategori', function($query) {
$query->where('NamaKategori', 'Sekilas Info');
})
->where('IsPublish', true)
->orderBy('created_at', 'desc')
->take(3)
->get();
return Inertia::render('welcome', [
'posts' => $pengumuman,
'undangan' => $undangan,
'peraturan' => $peraturan,
'sekilasInfo' => $sekilasInfo,
'runPosts' => $runPosts,
]);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class HukumController extends Controller
{
public function index()
{
try {
return Inertia::render('admin/hukum/index_hukum');
} catch (\Exception $e) {
Log::error('Error rendering view: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\JenisDokILRequest;
use App\Models\JenisDokIL;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class JenisDokILController extends Controller
{
public function index()
{
try {
$jenisdokil = JenisDokIL::latest()->get();
return Inertia::render('admin/jenisdokil/index_jenisdokil', ['jenisdokil' => $jenisdokil]);
} catch (\Exception $e) {
Log::error('Error fetching Jenis Dokumen Ilmiah: ' . $e->getMessage());
return back()->with('error', 'Failed to fetch Jenis Dokumen Ilmiah.');
}
}
public function store(JenisDokILRequest $request)
{
try {
JenisDokIL::create($request->validated());
return redirect()->route('admin.jenisdokil.index')->with('success', 'Jenis Dokumen Ilmiah created successfully.');
} catch (\Exception $e) {
Log::error('Error creating Jenis Dokumen Ilmiah: ' . $e->getMessage());
return back()->with('error', 'Failed to create Jenis Dokumen Ilmiah.');
}
}
public function update(JenisDokILRequest $request, JenisDokIL $jenisdokil)
{
try {
$jenisdokil->update($request->validated());
return redirect()->route('admin.jenisdokil.index')->with('success', 'Jenis Dokumen Ilmiah updated successfully.');
} catch (\Exception $e) {
Log::error('Error updating Jenis Dokumen Ilmiah: ' . $e->getMessage());
return back()->with('error', 'Failed to update Jenis Dokumen Ilmiah.');
}
}
public function destroy(JenisDokIL $jenisdokil)
{
try {
$jenisdokil->delete();
return redirect()->route('admin.jenisdokil.index')->with('success', 'Jenis Dokumen Ilmiah deleted successfully.');
} catch (\Exception $e) {
Log::error('Error deleting Jenis Dokumen Ilmiah: ' . $e->getMessage());
return back()->with('error', 'Failed to delete Jenis Dokumen Ilmiah.');
}
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\JenisKegiatanRequest;
use App\Models\JenisKegiatan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class JenisKegiatanController extends Controller
{
public function index()
{
try {
$jeniskegiatan = JenisKegiatan::latest()->get();
return Inertia::render('admin/kegiatan/index_jenis_kegiatan', ['jeniskegiatan' => $jeniskegiatan]);
} catch (\Exception $e) {
Log::error('Error fetching Jenis Kegiatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function store(JenisKegiatanRequest $request)
{
try {
$jeniskegiatan = JenisKegiatan::withTrashed()
->where('NamaJenisKegiatan', $request->NamaJenisKegiatan)
->first();
if ($jeniskegiatan) {
$jeniskegiatan->restore();
return redirect()->route('admin.jeniskegiatan.index')->with('success', 'Jenis Kegiatan berhasil dikembalikan.');
}
JenisKegiatan::create($request->validated());
return redirect()->route('admin.jeniskegiatan.index')->with('success', 'Jenis Kegiatan berhasil dibuat.');
} catch (\Exception $e) {
Log::error('Error creating Jenis Kegiatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function update(JenisKegiatanRequest $request, JenisKegiatan $jeniskegiatan)
{
try {
$jeniskegiatan->update($request->validated());
return redirect()->route('admin.jeniskegiatan.index')->with('success', 'Jenis Kegiatan berhasil diperbarui.');
} catch (\Exception $e) {
Log::error('Error updating Jenis Kegiatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function destroy(JenisKegiatan $jeniskegiatan)
{
try {
$jeniskegiatan->delete();
return redirect()->route('admin.jeniskegiatan.index')->with('success', 'Jenis Kegiatan berhasil dihapus.');
} catch (\Exception $e) {
Log::error('Error deleting Jenis Kegiatan: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers;
use App\Models\JenisSanksi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class JenisSanksiController extends Controller
{
public function index()
{
try {
$jenissanksi = JenisSanksi::latest()->get();
return Inertia::render('admin/jenissanksi/index_jenis_sanksi', ['jenissanksi' => $jenissanksi]);
} catch (\Exception $e) {
Log::error('Error fetching Jenis Sanksi: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function store(Request $request)
{
try {
JenisSanksi::create($request->all());
return redirect()->route('admin.jenissanksi.index')->with('success', 'Jenis Sanksi berhasil dibuat.');
} catch (\Exception $e) {
Log::error('Error creating Jenis Sanksi: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function update(Request $request, JenisSanksi $jenissanksi)
{
try {
$jenissanksi->update($request->all());
return redirect()->route('admin.jenissanksi.index')->with('success', 'Jenis Sanksi berhasil diperbarui.');
} catch (\Exception $e) {
Log::error('Error updating Jenis Sanksi: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
public function destroy(JenisSanksi $jenissanksi)
{
try {
$jenissanksi->delete();
return redirect()->route('admin.jenissanksi.index')->with('success', 'Jenis Sanksi berhasil dihapus.');
} catch (\Exception $e) {
Log::error('Error deleting Jenis Sanksi: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class PelaporanController extends Controller
{
public function index()
{
try {
return Inertia::render('admin/pelaporan/index_pelaporan');
} catch (\Exception $e) {
Log::error('Error rendering view: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\Log;
class PengumumanController extends Controller
{
public function index()
{
$posts = Post::with(['kategori', 'subkategori'])
->whereHas('kategori', function($query) {
$query->where('NamaKategori', 'Pengumuman');
})
->where('IsPublish', true)
->orderBy('created_at', 'desc')
->get();
Log::info('Posts data:', ['posts' => $posts->toArray()]); // Debug log
return Inertia::render('Pengumuman', [
'posts' => $posts
]);
}
public function show($slug)
{
$article = Post::with(['kategori', 'subkategori'])
->where('SlugPost', $slug)
->firstOrFail();
$relatedArticles = Post::with(['kategori', 'subkategori'])
->where('KategoriId', $article->KategoriId)
->where('PostId', '!=', $article->PostId)
->where('IsPublish', true)
->latest()
->take(5)
->get();
return Inertia::render('detail/PegumumanDetail', [
'article' => $article,
'relatedArticles' => $relatedArticles
]);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Inertia;
class PeraturanController extends Controller
{
public function index()
{
$posts = Post::with(['kategori', 'subkategori'])
->whereHas('kategori', function($query) {
$query->where('NamaKategori', 'Peraturan');
})
->where('IsPublish', true)
->orderBy('created_at', 'desc')
->get();
return Inertia::render('Peraturan', [
'posts' => $posts
]);
}
public function show($slug)
{
$article = Post::with(['kategori', 'subkategori'])
->where('SlugPost', $slug)
->firstOrFail();
$relatedArticles = Post::with(['kategori', 'subkategori'])
->where('KategoriId', $article->KategoriId)
->where('PostId', '!=', $article->PostId)
->where('IsPublish', true)
->latest()
->take(5)
->get();
return Inertia::render('detail/PeraturanDetail', [
'article' => $article,
'relatedArticles' => $relatedArticles
]);
}
}

View File

@ -10,6 +10,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Inertia\Inertia;
use Illuminate\Support\Facades\DB;
class PostController extends Controller
{
@ -32,34 +33,68 @@ class PostController extends Controller
$subkategori = SubKategori::all();
return Inertia::render('admin/post/add_post', [
'kategori' => $kategori,
'subkategori' => $subkategori
'kategori' => Kategori::all(),
'subkategori' => SubKategori::all(),
'existingPosts' => Post::select('JudulPost', 'KategoriId')->get()
]);
}
public function store(PostRequest $request)
{
try {
$data = $request->validated();
$data['IsPublish'] = $request->has('IsPublish');
DB::beginTransaction();
try {
$data['IsPublish'] = $request->boolean('IsPublish');
if ($request->hasFile('ImagePost')) {
$data['ImagePost'] = $request->file('ImagePost')->store('images/posts');
$file = $request->file('ImagePost');
if (!$file->isValid()) {
throw new \Exception('Invalid image file');
}
Post::create($data);
$data['ImagePost'] = $file->store('images/posts', 'public');
if ($data['ImagePost'] === false) {
throw new \Exception('Failed to store image');
}
}
$post = Post::create($data);
DB::commit();
return redirect()
->route('admin.post.index')
->with('success', 'Post berhasil ditambahkan');
return redirect()->route('admin.post.index')->with('success', 'Post berhasil ditambahkan');
} catch (\Exception $e) {
return redirect()->route('admin.post.index')->with('error', 'Post gagal ditambahkan');
DB::rollBack();
if (isset($data['ImagePost']) && Storage::disk('public')->exists($data['ImagePost'])) {
Storage::disk('public')->delete($data['ImagePost']);
}
Log::error('Error creating post: ' . $e->getMessage());
return redirect()
->route('admin.post.index')
->with('error', 'Post gagal ditambahkan: ' . $e->getMessage());
}
} catch (\Exception $e) {
Log::error('Validation error: ' . $e->getMessage());
return redirect()
->route('admin.post.index')
->with('error', 'Validasi gagal: ' . $e->getMessage());
}
}
public function edit(Post $post)
{
return Inertia::render('admin/post/edit_post', [
'post' => $post,
'kategori' => Kategori::all(),
@ -70,19 +105,30 @@ class PostController extends Controller
public function update(PostRequest $request, Post $post)
{
try {
$data = $request->validated();
$data = array_filter($request->validated(), function($value) {
return $value !== null;
});
if ($request->hasFile('ImagePost')) {
if ($post->ImagePost && Storage::disk('public')->exists($post->ImagePost)) {
Storage::disk('public')->delete($post->ImagePost);
}
$data['ImagePost'] = $request->file('ImagePost')->store('images/posts', 'public');
}
if (isset($data['IsPublish'])) {
$data['IsPublish'] = (bool) $data['IsPublish'];
}
$post->update($data);
return redirect()->route('admin.post.index')->with('success', 'Post berhasil diperbarui.');
return redirect()
->route('admin.post.index')
->with('success', 'Post berhasil diperbarui.');
} catch (\Exception $e) {
Log::error('Error updating Post: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
return back()->with('error', 'Terjadi kesalahan saat memperbarui post.');
}
}
@ -98,6 +144,4 @@ class PostController extends Controller
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Inertia;
class SearchController extends Controller
{
public function index(Request $request)
{
$query = $request->input('q');
$searchResults = Post::with(['kategori', 'subkategori'])
->where('IsPublish', true)
->where(function($q) use ($query) {
$q->where('JudulPost', 'ILIKE', "%{$query}%")
->orWhere('DescPost', 'ILIKE', "%{$query}%");
})
->latest()
->get();
return Inertia::render('Search', [
'searchResults' => $searchResults,
'searchQuery' => $query
]);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Inertia;
class SekilasInfoController extends Controller
{
public function show($slug)
{
$article = Post::with(['kategori', 'subkategori'])
->where('SlugPost', $slug)
->firstOrFail();
$relatedArticles = Post::with(['kategori', 'subkategori'])
->where('KategoriId', $article->KategoriId)
->where('PostId', '!=', $article->PostId)
->where('IsPublish', true)
->latest()
->take(5)
->get();
return Inertia::render('detail/SekilasInfoDetail', [
'article' => $article,
'relatedArticles' => $relatedArticles
]);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class TentangController extends Controller
{
public function index()
{
try {
return Inertia::render('admin/navBottom/index_tentang');
} catch (\Exception $e) {
Log::error('Error rendering view: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Inertia;
class UndanganController extends Controller
{
public function index()
{
$posts = Post::with(['kategori', 'subkategori'])
->whereHas('kategori', function($query) {
$query->where('NamaKategori', 'Undangan');
})
->where('IsPublish', true)
->orderBy('created_at', 'desc')
->get();
return Inertia::render('Undangan', [
'posts' => $posts
]);
}
public function show($slug)
{
$article = Post::with(['kategori', 'subkategori'])
->where('SlugPost', $slug)
->firstOrFail();
$relatedArticles = Post::with(['kategori', 'subkategori'])
->where('KategoriId', $article->KategoriId)
->where('PostId', '!=', $article->PostId)
->where('IsPublish', true)
->latest()
->take(5)
->get();
return Inertia::render('detail/UndanganDetail', [
'article' => $article,
'relatedArticles' => $relatedArticles
]);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class VerifPelaporanController extends Controller
{
public function index()
{
try {
return Inertia::render('admin/pelaporan/index_verif_pelaporan');
} catch (\Exception $e) {
Log::error('Error rendering view: ' . $e->getMessage());
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\VerifikatorRequest;
use App\Models\Verifikator;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;
class VerifikatorController extends Controller
{
public function index()
{
try {
$verifikator = Verifikator::latest()->get();
return Inertia::render('admin/verifikator/index_verifikator', ['verifikator' => $verifikator]);
} catch (\Exception $e) {
Log::error('Error fetching Verifikator: ' . $e->getMessage());
return back()->with('error', 'Failed to fetch verifikator.');
}
}
public function store(VerifikatorRequest $request)
{
try {
Verifikator::create($request->validated());
return redirect()->route('verifikator.index')->with('success', 'Verifikator created successfully.');
} catch (\Exception $e) {
Log::error('Error creating verifikator: ' . $e->getMessage());
return back()->with('error', 'Failed to create verifikator.');
}
}
public function update(VerifikatorRequest $request, Verifikator $verifikator)
{
try {
$verifikator->update($request->validated());
return redirect()->route('verifikator.index')->with('success', 'Verifikator updated successfully.');
} catch (\Exception $e) {
Log::error('Error updating verifikator: ' . $e->getMessage());
return back()->with('error', 'Failed to update verifikator.');
}
}
public function destroy(Verifikator $verifikator)
{
try {
$verifikator->delete();
return redirect()->route('verifikator.index')->with('success', 'Verifikator deleted successfully.');
} catch (\Exception $e) {
Log::error('Error deleting verifikator: ' . $e->getMessage());
return back()->with('error', 'Failed to delete verifikator.');
}
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class HistoryKegiatanRequest 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 [
'NamaHistoryKegiatan' => [
'required',
'string',
'max:255',
Rule::unique('HistoryKegiatan', 'NamaHistoryKegiatan')->whereNull('deleted_at') // Abaikan data yang dihapus (soft delete)
],
'IsPublish' => 'boolean',
];
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class HukumRequest 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 [
'PerusahaanId' => 'nullable|integer|exists:RefPerusahaan,id',
'JenisSanksiId' => 'nullable|integer|exists:RefJenisSanksi,id',
'SanksiNumber' => 'nullable|string|max:100',
'SanksiDate' => 'nullable|date',
'SanksiFile' => 'nullable|file|mimes:pdf|max:2048',
'StatusPenaatan' => 'required|integer|in:1,2,3',
'PenaatanNumber' => 'nullable|string|max:100',
'PenaatanDate' => 'nullable|date',
'PenaatanFile' => 'nullable|file|mimes:pdf,jpg,png|max:2048',
'IsDeleted' => 'nullable|boolean',
];
}
public function messages(): array
{
return [
'PerusahaanId.integer' => 'Perusahaan harus berupa angka.',
'PerusahaanId.exists' => 'Perusahaan tidak ditemukan.',
'JenisSanksiId.integer' => 'Jenis Sanksi harus berupa angka.',
'JenisSanksiId.exists' => 'Jenis Sanksi tidak ditemukan.',
'SanksiNumber.max' => 'Nomor SK Sanksi maksimal 100 karakter.',
'SanksiDate.date' => 'Tanggal SK Sanksi harus berupa format tanggal.',
'SanksiFile.mimes' => 'File Sanksi harus berupa PDF',
'SanksiFile.max' => 'Ukuran file maksimal 2MB.',
'StatusPenaatan.required'=> 'Status Penaatan wajib diisi.',
'StatusPenaatan.in' => 'Status Penaatan hanya boleh 1, 2, atau 3.',
'PenaatanNumber.max' => 'Nomor SK Penaatan maksimal 100 karakter.',
'PenaatanDate.date' => 'Tanggal SK Penaatan harus berupa format tanggal.',
'PenaatanFile.mimes' => 'File Penaatan harus berupa PDF',
'PenaatanFile.max' => 'Ukuran file maksimal 2MB.',
'IsDeleted.boolean' => 'IsDeleted hanya boleh bernilai 0 atau 1.',
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class JenisDokILRequest 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 [
'NamaJenisDokIL' => 'required|string|max:255',
'KodeJenisDokIL' => 'required|string|max:20',
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class JenisKegiatanRequest 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 [
'NamaJenisKegiatan' => [
'required',
'string',
'max:255',
Rule::unique('JenisKegiatan', 'NamaJenisKegiatan')->whereNull('deleted_at') // Abaikan data yang dihapus (soft delete)
],
'IsPublish' => 'boolean',
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class JenisSanksiRequest 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 [
'NamaJenisSanksi' => [
'required',
'string',
'max:255',
Rule::unique('JenisSanksi', 'NamaJenisSanksi')->whereNull('deleted_at') // Abaikan data yang dihapus (soft delete)
],
];
}
}

View File

@ -3,7 +3,7 @@
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Log;
class PostRequest extends FormRequest
{
@ -22,15 +22,53 @@ class PostRequest extends FormRequest
*/
public function rules(): array
{
if ($this->isMethod('POST')) {
return [
'KategoriId' => 'required|exists:Kategori,KategoriId',
'SubKategoriId' => [
'required',
Rule::exists('SubKategori', 'SubKategoriId')->where('KategoriId', $this->KategoriId),
],
'KategoriId' => 'required|integer|exists:Kategori,KategoriId',
'SubKategoriId' => 'required|integer|exists:SubKategori,SubKategoriId',
'JudulPost' => ['required', 'string', 'max:255'],
'SlugPost' => ['required', 'string', 'max:255'],
'DescPost' => ['required', 'string'],
'ImagePost' => ['nullable', 'image', 'mimes:jpg,png,webp', 'max:2048'],
'ImagePost' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
'IsPublish' => 'boolean',
];
}
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
return [
'KategoriId' => 'nullable|integer|exists:Kategori,KategoriId',
'SubKategoriId' => 'nullable|integer|exists:SubKategori,SubKategoriId',
'JudulPost' => ['nullable', 'string', 'max:255'],
'SlugPost' => ['nullable', 'string', 'max:255'],
'DescPost' => ['nullable', 'string'],
'ImagePost' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
'IsPublish' => 'nullable|boolean',
];
}
return [];
}
// Add this method to handle validation before rules are applied
protected function prepareForValidation()
{
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
// Only set these if they're not provided in the request
if (!$this->has('KategoriId')) {
$this->merge(['KategoriId' => $this->route('post')->KategoriId]);
}
if (!$this->has('SubKategoriId')) {
$this->merge(['SubKategoriId' => $this->route('post')->SubKategoriId]);
}
if (!$this->has('JudulPost')) {
$this->merge(['JudulPost' => $this->route('post')->JudulPost]);
}
if (!$this->has('SlugPost')) {
$this->merge(['SlugPost' => $this->route('post')->SlugPost]);
}
if (!$this->has('DescPost')) {
$this->merge(['DescPost' => $this->route('post')->DescPost]);
}
}
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class VerifikatorRequest 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 [
'NamaUnitKerja' => 'required|string|max:255',
'NamaKepala' => 'required|string|max:255',
'NIP' => 'required|string|max:255',
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class HistoryKegiatan extends Model
{
use HasFactory; use SoftDeletes;
protected $table = 'HistoryKegiatan';
protected $dates = ['deleted_at'];
protected $primaryKey = 'HistoryKegiatanId';
protected $fillable = [
'HistoryKegiatanId',
'NamaHistoryKegiatan',
'IsPublish',
];
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Hukum extends Model
{
use HasFactory;
protected $table = 'Hukum';
protected $fillable = [
'PerusahaanId',
'JenisSanksiId',
'SanksiNumber',
'SanksiDate',
'SanksiFile',
'StatusPenaatan',
'PenaatanNumber',
'PenaatanDate',
'PenaatanFile',
'IsDeleted',
];
public function perusahaan()
{
return $this->belongsTo(Perusahaan::class, 'PerusahaanId');
}
/**
* Relasi ke tabel jenis sanksi.
*/
public function jenisSanksi()
{
return $this->belongsTo(JenisSanksi::class, 'JenisSanksiId');
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class JenisDokIL extends Model
{
protected $table = 'JenisDokIL';
protected $primaryKey = 'JenisDokILId';
protected $fillable = [
'KodeJenisDokIL',
'NamaJenisDokIL'
];
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class JenisKegiatan extends Model
{
use HasFactory; use SoftDeletes;
protected $table = 'JenisKegiatan';
protected $dates = ['deleted_at'];
protected $primaryKey = 'JenisKegiatanId';
protected $fillable = [
'NamaJenisKegiatan',
'IsPublish',
];
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class JenisSanksi extends Model
{
use HasFactory; use SoftDeletes;
protected $table = 'JenisSanksi';
protected $primaryKey = 'JenisSanksiId';
protected $fillable = ['NamaJenisSanksi'];
protected $dates = ['deleted_at'];
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Kabupaten extends Model
{
protected $table = 'Kabupaten';
protected $primaryKey = 'KabupatenId';
protected $fillable = [
'KabupatenId',
'NamaKabupaten',
];
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Kecamatan extends Model
{
protected $table = 'Kecamatan';
protected $primaryKey = 'KecamatanId';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'KecamatanId',
'KabupatenId',
'NamaKecamatan'
];
public function kabupaten()
{
return $this->belongsTo(Kabupaten::class, 'KabupatenId', 'KabupatenId');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Kelurahan extends Model
{
protected $table = 'Kelurahan';
protected $primaryKey = 'KelurahanId';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = ['KelurahanId', 'KecamatanId', 'NamaKelurahan'];
public function kecamatan()
{
return $this->belongsTo(Kecamatan::class, 'KecamatanId', 'KecamatanId');
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Perusahaan extends Model
{
protected $table = 'Perusahaan';
protected $primaryKey = 'PerusahaanId';
protected $fillable = [
'NomorInduk',
'JenisKegiatanId',
'NamaPerusahaan',
'Alamat',
'KelurahanId',
'KodePos',
'Telepon',
'Fax',
'Email',
'Lintang',
'Bujur',
'CPNama',
'CPTelepon',
'ILNomor',
'ILTanggal',
'JenisDokILId',
'VerifikatorId',
'ReportLocked',
'IsPublish',
'ILDokumen',
'ILPdlNomor',
'ILPdlJenis',
'DocPdlOrig',
'DocPdlHash',
'DocPdlPath',
'ILPdlTanggal',
'Kawasan'
];
protected $casts = [
'ReportLocked' => 'boolean',
'IsPublish' => 'boolean',
'ILPdlTanggal' => 'date',
];
// Relationship with JenisKegiatan
public function jenisKegiatan(): BelongsTo
{
return $this->belongsTo(JenisKegiatan::class, 'JenisKegiatanId', 'JenisKegiatanId');
}
// Relationship with Kelurahan
public function kelurahan(): BelongsTo
{
return $this->belongsTo(Kelurahan::class, 'KelurahanId', 'KelurahanId');
}
// Relationship with JenisDokIL
public function jenisDokIL(): BelongsTo
{
return $this->belongsTo(JenisDokIL::class, 'JenisDokILId', 'JenisDokILId');
}
// Relationship with JenisDokIL for PDL
public function jenisDokILPdl(): BelongsTo
{
return $this->belongsTo(JenisDokIL::class, 'ILPdlJenis', 'JenisDokILId');
}
// Relationship with Verifikator
public function verifikator(): BelongsTo
{
return $this->belongsTo(Verifikator::class, 'VerifikatorId', 'VerifikatorId');
}
// Self-referential relationship for Kawasan
public function kawasan(): BelongsTo
{
return $this->belongsTo(Perusahaan::class, 'Kawasan', 'PerusahaanId');
}
}

View File

@ -37,12 +37,12 @@ class Post extends Model
return \Carbon\Carbon::parse($this->created_at)->translatedFormat('d F Y');
}
public static function boot()
{
parent::boot();
// public static function boot()
// {
// parent::boot();
static::creating(function ($post) {
$post->Slug = Str::slug($post->JudulPost);
});
}
// static::creating(function ($post) {
// $post->SlugPost = Str::slug($post->JudulPost);
// });
// }
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Verifikator extends Model
{
use HasFactory;
protected $table = 'Verifikator';
protected $primaryKey = 'VerifikatorId';
protected $fillable = ['NamaUnitKerja', 'NamaKepala', 'NIP'];
}

View File

@ -2,8 +2,9 @@
namespace App\Providers;
use App\Models\Post;
use Illuminate\Support\ServiceProvider;
use Inertia\Inertia;
class AppServiceProvider extends ServiceProvider
{
@ -20,6 +21,11 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
Inertia::share([
'runPosts' => fn () => Post::with(['kategori', 'subkategori'])
->where('IsPublish', true)
->orderBy('created_at', 'desc')
->get(),
]);
}
}

View File

@ -13,7 +13,7 @@ return [
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
'default' => env('FILESYSTEM_DISK', 'public'),
/*
|--------------------------------------------------------------------------

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('historykegiatan', function (Blueprint $table) {
$table->id('HistoryKegiatanId');
$table->string('NamaHistoryKegiatan');
$table->boolean('IsPublish')->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('historykegiatan');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jeniskegiatan', function (Blueprint $table) {
$table->id('JenisKegiatanId');
$table->string('NamaJenisKegiatan');
$table->boolean('IsPublish')->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jeniskegiatan');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('JenisKegiatan', function (Blueprint $table) {
$table->softDeletes()->after('IsPublish');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('JenisKegiatan', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('HistoryKegiatan', function (Blueprint $table) {
$table->softDeletes()->after('IsPublish');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('HistoryKegiatan', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('Kategori', function (Blueprint $table) {
$table->dropUnique(['NamaKategori']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('Kategori', function (Blueprint $table) {
$table->unique('NamaKategori');
});
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('PeriodePelaporan', function (Blueprint $table) {
$table->id('PeriodePelaporanId');
$table->string('NamaPeriodePelaporan');
$table->date('BulanAwal');
$table->date('BulanSelesai');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('PeriodePelaporan', function (Blueprint $table) {
//
});
}
};

View File

@ -0,0 +1,64 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('Perusahaan', function (Blueprint $table) {
$table->id('PerusahaanId');
$table->string('NomorInduk')->nullable();
$table->unsignedBigInteger('JenisKegiatanId');
$table->foreign('JenisKegiatanId')->references('JenisKegiatanId')->on('JenisKegiatan')->onDelete('cascade');
$table->string('NamaPerusahaan');
$table->string('Alamat')->nullable();
$table->unsignedBigInteger('KelurahanId');
$table->foreign('KelurahanId')->references('KelurahanId')->on('Kelurahan')->onDelete('cascade');
$table->string('KodePos')->nullable();
$table->string('Telepon')->nullable();
$table->string('Fax')->nullable();
$table->string('Email')->nullable();
$table->string('Lintang')->nullable();
$table->string('Bujur')->nullable();
$table->string('CPNama')->nullable();
$table->string('CPTelepon')->nullable();
$table->string('ILNomor')->nullable();
$table->string('ILTanggal')->nullable();
$table->unsignedBigInteger('JenisDokILId');
$table->foreign('JenisDokILId')->references('JenisDokILId')->on('JenisDokIL')->onDelete('cascade');
$table->unsignedBigInteger('VerifikatorId');
$table->foreign('VerifikatorId')->references('VerifikatorId')->on('Verifikator')->onDelete('cascade');
$table->boolean('ReportLocked')->default(1);
$table->boolean('IsPublish')->default(0);
$table->string('ILDokumen')->nullable();
$table->string('ILPdlNomor')->default('n')->comment('Dok IL versi PDL');
$table->unsignedInteger('ILPdlJenis')->nullable()->comment('id dari refjenisdokil');
$table->foreign('ILPdlJenis')->references('JenisDokILId')->on('JenisDokIL')->onDelete('set null');
$table->string('DocPdlOrig')->nullable()->comment('nama file original');
$table->string('DocPdlHash')->nullable()->comment('nama file hash');
$table->string('DocPdlPath')->nullable()->comment('upload path');
$table->date('ILPdlTanggal')->nullable();
$table->unsignedInteger('Kawasan')->nullable()->comment('id dari perusahaan, digunakan utk pengelola kawasan');
$table->foreign('Kawasan')->references('PerusahaanId')->on('Perusahaan')->onDelete('set null');
$table->timestamps();
$table->index('NomorInduk', 'IndexNomorInduk');
$table->index('KelurahanId', 'IndexKelurahanId');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('Perusahaan');
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('Kelurahan', function (Blueprint $table) {
$table->id('KelurahanId');
$table->unsignedBigInteger('KecamatanId');
$table->foreign('KecamatanId')->references('KecamatanId')->on('Kecamatan')->onDelete('cascade');
$table->string('NamaKelurahan');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('Kelurahan');
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('Kecamatan', function (Blueprint $table) {
$table->id('KecamatanId');
$table->unsignedBigInteger('KabupatenId');
$table->foreign('KabupatenId')->references('KabupatenId')->on('Kabupaten')->onDelete('cascade');
$table->string('NamaKecamatan');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('Kecamatan');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('Kabupaten', function (Blueprint $table) {
$table->id('KabupatenId');
$table->string('NamaKabupaten');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('Kabupaten');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('JenisDokIL', function (Blueprint $table) {
$table->id('JenisDokILId');
$table->string('KodeJenisDokIL')->unique();
$table->string('NamaJenisDokIL');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('JenisDokIL');
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('Verifikator', function (Blueprint $table) {
$table->id('VerifikatorId');
$table->string('NamaUnitKerja');
$table->string('NamaKepala');
$table->string('NIP');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('Verifikator');
}
};

View File

@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('Pelaporan', function (Blueprint $table) {
$table->id('PelaporanId');
$table->unsignedInteger('PeriodePelaporanId');
$table->foreign('PeriodePelaporanId')->references('PeriodePelaporanId')->on('PeriodePelaporan')->onDelete('cascade');
$table->year('Tahun');
$table->unsignedInteger('PerusahaanId');
$table->foreign('PerusahaanId')->references('PerusahaanId')->on('Perusahaan')->onDelete('cascade');
$table->float('SKL');
$table->float('SPL');
$table->float('SKL_IL');
$table->float('SKL_AL');
$table->float('SKL_LB3');
$table->float('SKL_SB');
$table->float('SKL_BS');
$table->float('SKL_STB');
$table->float('SKL_LP');
$table->float('SKL_KDM');
$table->float('SPL_AL');
$table->float('SPL_LB3');
$table->float('SPL_SB');
$table->float('SPL_BS');
$table->float('SPL_STB');
$table->float('SPL_LP');
$table->float('SPL_KDM');
$table->string('IsChecked', 100)->nullable();
$table->timestamps();
$table->index('PeriodePelaporanId', 'IndexPeriodePelaporanId');
$table->index('Tahun', 'IndexTahun');
$table->index('PerusahaanId', 'IndexPerusahaanId');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('Pelaporan');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('JenisSanksi', function (Blueprint $table) {
$table->id('JenisSanksiId');
$table->string('NamaJenisSanksi');
$table->softDeletes();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('JenisSanksi');
}
};

View File

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('Hukum', function (Blueprint $table) {
$table->id('HukumId');
$table->unsignedInteger('PerusahaanId')->nullable()->comment('id dari Perusahaan');
$table->foreign('PerusahaanId')->references('PerusahaanId')->on('Perusahaan')->onDelete('cascade');
$table->unsignedInteger('JenisSanksiId')->nullable()->comment('id dari Jenis Sanksi');
$table->foreign('JenisSanksiId')->references('JenisSanksiId')->on('JenisSanksi')->onDelete('cascade');
$table->string('SanksiNumber', 100)->nullable()->comment('nomor SK Sanksi');
$table->date('SanksiDate')->nullable()->comment('tanggal SK Sanksi');
$table->string('SanksiFile', 255)->nullable()->comment('path + orig filename + id perusahaan + epoch time');
$table->unsignedTinyInteger('StatusPenaatan')->default(1)->comment('1=pengawasan, 2=peningkatan sanksi, 3=taat');
$table->string('PenaatanNumber', 100)->nullable()->comment('nomor SK penaatan');
$table->date('PenaatanDate')->nullable()->comment('tanggal SK penaatan');
$table->string('PenaatanFile', 255)->nullable()->comment('path + orig filename + id perusahaan + epoch time');
$table->unsignedTinyInteger('IsDeleted')->default(0)->comment('0=false, 1=true');
$table->index('PerusahaanId');
$table->index('JenisSanksiId');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('Hukum');
}
};

View File

@ -0,0 +1,38 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Http;
use App\Models\Kabupaten;
use Illuminate\Support\Facades\Log;
class KabupatenTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Ambil data kabupaten dari API
$response = Http::get('https://api-wilayah.dinaslhdki.id/api/kabupaten/dkj/search');
// Loop data kabupaten yang diterima dari API
foreach ($response->json() as $kabupaten) {
// Convert the id format and map the data
$cleanId = str_replace('.', '', $kabupaten['id']);
Kabupaten::create([
'KabupatenId' => $cleanId,
'NamaKabupaten' => $kabupaten['data']
]);
// Log data kabupaten yang sedang di-seed
Log::info('Seeding kabupaten: ', [
'KabupatenId' => $cleanId,
'NamaKabupaten' => $kabupaten['data']
]);
}
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Database\Seeders;
use App\Models\Kecamatan;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use App\Models\Kabupaten;
class KecamatanTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$kabupatens = Kabupaten::all();
foreach ($kabupatens as $kabupaten) {
$kabId = substr($kabupaten->KabupatenId, 0, 2) . '.' .
substr($kabupaten->KabupatenId, 2, 2);
$response = Http::get("https://api-wilayah.dinaslhdki.id/api/kecamatan/search?kab={$kabId}");
foreach ($response->json() as $kecamatan) {
$cleanId = str_replace('.', '', $kecamatan['id']);
Kecamatan::create([
'KecamatanId' => $cleanId,
'KabupatenId' => $kabupaten->KabupatenId,
'NamaKecamatan' => $kecamatan['data']
]);
// Log the seeding process
Log::info('Seeding kecamatan: ', [
'KecamatanId' => $cleanId,
'KabupatenId' => $kabupaten->KabupatenId,
'NamaKecamatan' => $kecamatan['data']
]);
}
}
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Database\Seeders;
use App\Models\Kecamatan;
use App\Models\Kelurahan;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class KelurahanTableSeeder extends Seeder
{
public function run(): void
{
$kecamatans = Kecamatan::all();
foreach ($kecamatans as $kecamatan) {
$kecId = substr($kecamatan->KecamatanId, 0, 2) . '.' .
substr($kecamatan->KecamatanId, 2, 2) . '.' .
substr($kecamatan->KecamatanId, 4, 2);
$response = Http::get("https://api-wilayah.dinaslhdki.id/api/kelurahan/search?kec={$kecId}");
foreach ($response->json() as $kelurahan) {
$cleanId = str_replace('.', '', $kelurahan['id']);
$cleanName = str_replace("\n", ' ', $kelurahan['data']);
Kelurahan::create([
'KelurahanId' => $cleanId,
'KecamatanId' => $kecamatan->KecamatanId,
'NamaKelurahan' => $cleanName
]);
Log::info('Seeding kelurahan: ', [
'KelurahanId' => $cleanId,
'KecamatanId' => $kecamatan->KecamatanId,
'NamaKelurahan' => $cleanName
]);
}
}
}
}

651
package-lock.json generated
View File

@ -14,20 +14,25 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.2.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.3",
"@reduxjs/toolkit": "^2.5.1",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^5.1.1",
"@types/react-redux": "^7.1.34",
"apexcharts": "^4.4.0",
"ckeditor4-react": "^4.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"framer-motion": "^12.4.3",
"highlight.js": "^11.11.1",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.2",
@ -43,10 +48,12 @@
"react-redux": "^9.2.0",
"react-resizable-panels": "^2.0.19",
"react-router-dom": "^7.1.4",
"react-select": "^5.10.0",
"react-type-animation": "^3.2.0",
"recharts": "^2.15.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"tinymce": "^7.6.1",
"vaul": "^1.1.2",
"ziggy-js": "^2.5.0",
"zod": "^3.23.8"
@ -101,7 +108,6 @@
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
@ -157,7 +163,6 @@
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.26.5",
@ -191,7 +196,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.25.9",
@ -233,7 +237,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@ -243,7 +246,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@ -277,7 +279,6 @@
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.7"
@ -337,7 +338,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.25.9",
@ -352,7 +352,6 @@
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
"integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
@ -371,7 +370,6 @@
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@ -381,6 +379,126 @@
"node": ">=6.9.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"license": "MIT"
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/react": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@ -1729,6 +1847,71 @@
}
}
},
"node_modules/@radix-ui/react-progress": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz",
"integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
@ -2125,6 +2308,134 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
"integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-roving-focus": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
"integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
"integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-collection": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
@ -2839,6 +3150,26 @@
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
}
},
"node_modules/@tanstack/react-table": {
"version": "8.21.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz",
"integrity": "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==",
"license": "MIT",
"dependencies": {
"@tanstack/table-core": "8.21.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz",
@ -2857,6 +3188,19 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.21.2",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz",
"integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.3.tgz",
@ -2868,6 +3212,20 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tinymce/tinymce-react": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-5.1.1.tgz",
"integrity": "sha512-DQ0wpvnf/9z8RsOEAmrWZ1DN1PKqcQHfU+DpM3llLze7FHmxVtzuN8O+FYh0oAAF4stzAXwiCIVacfqjMwRieQ==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.6.2",
"tinymce": "^7.0.0 || ^6.0.0 || ^5.5.1"
},
"peerDependencies": {
"react": "^18.0.0 || ^17.0.1 || ^16.7.0",
"react-dom": "^18.0.0 || ^17.0.1 || ^16.7.0"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -3016,6 +3374,12 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@ -3078,6 +3442,15 @@
"@babel/runtime": "^7.9.2"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@ -3242,6 +3615,21 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -3361,6 +3749,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@ -3547,6 +3944,31 @@
"node": ">=18"
}
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"license": "MIT",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
@ -3723,7 +4145,6 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -3878,6 +4299,15 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -3957,6 +4387,18 @@
"node": ">=6"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@ -4033,6 +4475,12 @@
"node": ">=8"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"license": "MIT"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@ -4099,6 +4547,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.4.3",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.3.tgz",
"integrity": "sha512-rsMeO7w3dKyNG09o3cGwSH49iHU+VgDmfSSfsX+wfkO3zDA6WWkh4sUsMXd155YROjZP+7FTIhDrBYfgZeHjKQ==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.0.0",
"motion-utils": "^12.0.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -4223,7 +4698,6 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@ -4379,6 +4853,22 @@
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@ -4404,6 +4894,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"license": "MIT"
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -4544,7 +5040,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@ -4553,6 +5048,12 @@
"node": ">=6"
}
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -4658,6 +5159,12 @@
"node": ">= 0.4"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -4737,11 +5244,25 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/motion-dom": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz",
"integrity": "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.0.0"
}
},
"node_modules/motion-utils": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz",
"integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/mz": {
@ -4904,6 +5425,36 @@
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"license": "BSD-3-Clause"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -4941,6 +5492,15 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/perfect-scrollbar": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.6.tgz",
@ -5487,6 +6047,27 @@
"react-dom": ">=18"
}
},
"node_modules/react-select": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.0.tgz",
"integrity": "sha512-k96gw+i6N3ExgDwPIg0lUPmexl1ygPe6u5BdQFNBhkpbwroIgCNXdubtIzHfThYXYYTubwOBafoMnn7ruEP1xA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.8.1",
"@floating-ui/dom": "^1.0.1",
"@types/react-transition-group": "^4.4.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.6.0",
"react-transition-group": "^4.3.0",
"use-isomorphic-layout-effect": "^1.2.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
@ -5677,6 +6258,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@ -5915,6 +6505,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -6020,6 +6619,12 @@
"node": ">=8"
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@ -6137,6 +6742,12 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinymce": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.6.1.tgz",
"integrity": "sha512-5cHhaAoyyTHfAVTInNfoSp0KkUHmeVUbGSu37QKQbOFIPqxYPhqBiaLm1WVLgoNBYOHRProVc3xzxnNTeWHyoQ==",
"license": "GPL-2.0-or-later"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -6246,6 +6857,20 @@
}
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz",
"integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",

View File

@ -34,20 +34,25 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.2.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.3",
"@reduxjs/toolkit": "^2.5.1",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^5.1.1",
"@types/react-redux": "^7.1.34",
"apexcharts": "^4.4.0",
"ckeditor4-react": "^4.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"framer-motion": "^12.4.3",
"highlight.js": "^11.11.1",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.2",
@ -63,10 +68,12 @@
"react-redux": "^9.2.0",
"react-resizable-panels": "^2.0.19",
"react-router-dom": "^7.1.4",
"react-select": "^5.10.0",
"react-type-animation": "^3.2.0",
"recharts": "^2.15.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"tinymce": "^7.6.1",
"vaul": "^1.1.2",
"ziggy-js": "^2.5.0",
"zod": "^3.23.8"

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@ -2,72 +2,10 @@ import React from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowRight } from "lucide-react";
import { Link } from "@inertiajs/react";
// const pengumumans = [
// {
// id: 1,
// title_page: "Pengumuman",
// alt_image: "Pengumuman",
// date: "16 Januari 2025",
// title: "Pelatihan & Sertifikasi Online Bidang Pengendalian Pencemaran Air Dan Udara",
// description:
// "Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan Tersebut Bekerjasama Dengan Lembaga P...",
// image: "/assets/img1.jpg",
// },
// {
// id: 2,
// title_page: "Pengumuman",
// alt_image: "Pengumuman",
// date: "12 Desember 2024",
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
// description:
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
// image: "/assets/img1.jpg",
// },
// {
// id: 3,
// title_page: "Pengumuman",
// alt_image: "Pengumuman",
// date: "12 Desember 2024",
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
// description:
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
// image: "/assets/img1.jpg",
// },
// {
// id: 4,
// title_page: "Pengumuman",
// alt_image: "Pengumuman",
// date: "12 Desember 2024",
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
// description:
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
// image: "/assets/img1.jpg",
// },
// {
// id: 5,
// title_page: "Pengumuman",
// alt_image: "Pengumuman",
// date: "12 Desember 2024",
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
// description:
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
// image: "/assets/img1.jpg",
// },
// {
// id: 6,
// title_page: "Pengumuman",
// alt_image: "Pengumuman",
// date: "12 Desember 2024",
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
// description:
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
// image: "/assets/img1.jpg",
// },
// ];
interface SubKategori {
SubKategoriId: number;
NamaSubKategori: string;
@ -95,24 +33,39 @@ interface CardPengumumanProps {
}
const CardPengumuman = ({ posts }: CardPengumumanProps) => {
return (
<section className="container max-w-7xl py-8 px-6">
{/* List of Announcements */}
const subcategories = Array.from(
new Set(posts.map((item) => item.subkategori?.NamaSubKategori))
).filter(Boolean);
const filterPosts = (subkategori: string | null) => {
if (!subkategori) return posts;
return posts.filter(
(item) => item.subkategori?.NamaSubKategori === subkategori
);
};
const [visibleItems, setVisibleItems] = React.useState(3);
const [activeTab, setActiveTab] = React.useState("all");
const loadMore = () => {
setVisibleItems((prev) => prev + 3);
};
const PostGrid = ({ items }: { items: Post[] }) => (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{posts.map((post) => (
<Card key={post.PostId} className="md:p-4">
{items.slice(0, visibleItems).map((item) => (
<Card key={item.PostId} className="md:p-4">
<img
src={`/storage/${post.ImagePost}`}
alt={post.JudulPost}
src={`/storage/${item.ImagePost}`}
alt={item.JudulPost}
className="rounded-md"
/>
<CardContent className="p-4">
<Badge className="bg-red-600 text-white">
{post.kategori?.NamaKategori} |{" "}
{post.subkategori?.NamaSubKategori}
{item.subkategori?.NamaSubKategori}
</Badge>
<p className="text-gray-500 text-sm mt-2">
{new Date(post.created_at).toLocaleDateString(
{new Date(item.created_at).toLocaleDateString(
"id-ID",
{
day: "numeric",
@ -122,12 +75,16 @@ const CardPengumuman = ({ posts }: CardPengumumanProps) => {
)}
</p>
<h3 className="text-md font-semibold mt-2 text-gray-900">
{post.JudulPost}
{item.JudulPost}
</h3>
<p className="text-sm text-gray-600 mt-2">
{post.DescPost.replace(/<[^>]*>/g, "")}
{item.DescPost.replace(/<[^>]*>/g, "").slice(
0,
160
)}
...
</p>
<Link href={`/post/${post.SlugPost}`}>
<Link href={route("pengumuman.show", item.SlugPost)}>
<Button
variant="link"
className="text-red-600 mt-2 pl-0"
@ -140,11 +97,69 @@ const CardPengumuman = ({ posts }: CardPengumumanProps) => {
</Card>
))}
</div>
);
const handleTabChange = (value: string) => {
setActiveTab(value);
setVisibleItems(3); // Reset visible items when changing tabs
};
return (
<section className="container max-w-7xl py-8 px-6">
{/* List of Announcements */}
<div className="flex justify-center items-center w-full">
<Tabs
defaultValue="all"
className="w-full max-w-6xl"
onValueChange={handleTabChange}
>
<div className="flex justify-center">
<TabsList className="mb-6">
<TabsTrigger value="all">Semua</TabsTrigger>
{subcategories.map((subcat) => (
<TabsTrigger
key={subcat}
value={subcat as string}
>
{subcat}
</TabsTrigger>
))}
</TabsList>
</div>
<TabsContent value="all">
<PostGrid items={filterPosts(null)} />
{visibleItems < filterPosts(null).length && (
<div className="flex justify-center mt-8">
<Button variant="link" className="text-green-700">
<Button
variant="link"
className="text-green-700"
onClick={loadMore}
>
Lihat Lebih Banyak
</Button>
</div>
)}
</TabsContent>
{subcategories.map((subcat) => (
<TabsContent key={subcat} value={subcat as string}>
<PostGrid items={filterPosts(subcat as string)} />
{visibleItems <
filterPosts(subcat as string).length && (
<div className="flex justify-center mt-8">
<Button
variant="link"
className="text-green-700"
onClick={loadMore}
>
Lihat Lebih Banyak
</Button>
</div>
)}
</TabsContent>
))}
</Tabs>
</div>
</section>
);
};

View File

@ -2,96 +2,89 @@ import React from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowRight } from "lucide-react";
import { Link } from "@inertiajs/react";
const peraturans = [
{
id: 1,
title_page: "Peraturan",
alt_image: "Peraturan",
date: "16 Januari 2025",
title: "Pelatihan & Sertifikasi Online Bidang Pengendalian Pencemaran Air Dan Udara",
description:
"Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan Tersebut Bekerjasama Dengan Lembaga P...",
image: "/assets/img1.jpg",
},
{
id: 2,
title_page: "Peraturan",
alt_image: "Peraturan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
{
id: 3,
title_page: "Peraturan",
alt_image: "Peraturan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
{
id: 4,
title_page: "Peraturan",
alt_image: "Peraturan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
{
id: 5,
title_page: "Peraturan",
alt_image: "Peraturan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
{
id: 6,
title_page: "Peraturan",
alt_image: "Peraturan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
];
interface SubKategori {
SubKategoriId: number;
NamaSubKategori: string;
}
const CardPeraturan = () => {
return (
<section className="container max-w-7xl py-8 px-6">
{/* List of Announcements */}
interface Kategori {
KategoriId: number;
NamaKategori: string;
}
interface Post {
PostId: number;
JudulPost: string;
DescPost: string;
SlugPost: string;
ImagePost: string;
IsPublish: boolean;
created_at: string;
kategori?: Kategori;
subkategori?: SubKategori;
}
interface CardPeraturanProps {
posts: Post[];
}
const CardPeraturan = ({ posts }: CardPeraturanProps) => {
const subcategories = Array.from(
new Set(posts.map((item) => item.subkategori?.NamaSubKategori))
).filter(Boolean);
const filterPosts = (subkategori: string | null) => {
if (!subkategori) return posts;
return posts.filter(
(item) => item.subkategori?.NamaSubKategori === subkategori
);
};
const [visibleItems, setVisibleItems] = React.useState(3);
const [activeTab, setActiveTab] = React.useState("all");
const loadMore = () => {
setVisibleItems((prev) => prev + 3);
};
const PostGrid = ({ items }: { items: Post[] }) => (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{peraturans.map((item) => (
<Card key={item.id} className="md:p-4">
{items.slice(0, visibleItems).map((item) => (
<Card key={item.PostId} className="md:p-4">
<img
src={item.image}
alt={item.alt_image}
src={`/storage/${item.ImagePost}`}
alt={item.JudulPost}
className="rounded-md"
/>
<CardContent className="p-4">
<Badge className="bg-red-600 text-white">
{item.title_page}
{item.subkategori?.NamaSubKategori}
</Badge>
<p className="text-gray-500 text-sm mt-2">
{item.date}
{new Date(item.created_at).toLocaleDateString(
"id-ID",
{
day: "numeric",
month: "long",
year: "numeric",
}
)}
</p>
<h3 className="text-md font-semibold mt-2 text-gray-900">
{item.title}
{item.JudulPost}
</h3>
<p className="text-sm text-gray-600 mt-2">
{item.description}
{item.DescPost.replace(/<[^>]*>/g, "").slice(
0,
160
)}
...
</p>
<Link href={route("peraturan.show", item.SlugPost)}>
<Button
variant="link"
className="text-red-600 mt-2 pl-0"
@ -99,15 +92,74 @@ const CardPeraturan = () => {
Baca Selengkapnya{" "}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</CardContent>
</Card>
))}
</div>
);
const handleTabChange = (value: string) => {
setActiveTab(value);
setVisibleItems(3); // Reset visible items when changing tabs
};
return (
<section className="container max-w-7xl py-8 px-6">
{/* List of Peraturan */}
<div className="flex justify-center items-center w-full">
<Tabs
defaultValue="all"
className="w-full max-w-6xl"
onValueChange={handleTabChange}
>
<div className="flex justify-center">
<TabsList className="mb-6">
<TabsTrigger value="all">Semua</TabsTrigger>
{subcategories.map((subcat) => (
<TabsTrigger
key={subcat}
value={subcat as string}
>
{subcat}
</TabsTrigger>
))}
</TabsList>
</div>
<TabsContent value="all">
<PostGrid items={filterPosts(null)} />
{visibleItems < filterPosts(null).length && (
<div className="flex justify-center mt-8">
<Button variant="link" className="text-green-700">
<Button
variant="link"
className="text-green-700"
onClick={loadMore}
>
Lihat Lebih Banyak
</Button>
</div>
)}
</TabsContent>
{subcategories.map((subcat) => (
<TabsContent key={subcat} value={subcat as string}>
<PostGrid items={filterPosts(subcat as string)} />
{visibleItems <
filterPosts(subcat as string).length && (
<div className="flex justify-center mt-8">
<Button
variant="link"
className="text-green-700"
onClick={loadMore}
>
Lihat Lebih Banyak
</Button>
</div>
)}
</TabsContent>
))}
</Tabs>
</div>
</section>
);
};

View File

@ -2,96 +2,89 @@ import React from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowRight } from "lucide-react";
import { Link } from "@inertiajs/react";
const undangans = [
{
id: 1,
title_page: "Undangan",
alt_image: "Undangan",
date: "16 Januari 2025",
title: "Pelatihan & Sertifikasi Online Bidang Pengendalian Pencemaran Air Dan Udara",
description:
"Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan Tersebut Bekerjasama Dengan Lembaga P...",
image: "/assets/img1.jpg",
},
{
id: 2,
title_page: "Undangan",
alt_image: "Undangan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
{
id: 3,
title_page: "Undangan",
alt_image: "Undangan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
{
id: 4,
title_page: "Undangan",
alt_image: "Undangan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
{
id: 5,
title_page: "Undangan",
alt_image: "Undangan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
{
id: 6,
title_page: "Undangan",
alt_image: "Undangan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
];
interface SubKategori {
SubKategoriId: number;
NamaSubKategori: string;
}
const CardUndangan = () => {
return (
<section className="container max-w-7xl py-8 px-6">
{/* List of Announcements */}
interface Kategori {
KategoriId: number;
NamaKategori: string;
}
interface Post {
PostId: number;
JudulPost: string;
DescPost: string;
SlugPost: string;
ImagePost: string;
IsPublish: boolean;
created_at: string;
kategori?: Kategori;
subkategori?: SubKategori;
}
interface CardUndanganProps {
posts: Post[];
}
const CardUndangan = ({ posts }: CardUndanganProps) => {
const subcategories = Array.from(
new Set(posts.map((item) => item.subkategori?.NamaSubKategori))
).filter(Boolean);
const filterPosts = (subkategori: string | null) => {
if (!subkategori) return posts;
return posts.filter(
(item) => item.subkategori?.NamaSubKategori === subkategori
);
};
const [visibleItems, setVisibleItems] = React.useState(3);
const [activeTab, setActiveTab] = React.useState("all");
const loadMore = () => {
setVisibleItems((prev) => prev + 3);
};
const PostGrid = ({ items }: { items: Post[] }) => (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{undangans.map((item) => (
<Card key={item.id} className="md:p-4">
{items.slice(0, visibleItems).map((item) => (
<Card key={item.PostId} className="md:p-4">
<img
src={item.image}
alt={item.alt_image}
src={`/storage/${item.ImagePost}`}
alt={item.JudulPost}
className="rounded-md"
/>
<CardContent className="p-4">
<Badge className="bg-red-600 text-white">
{item.title_page}
{item.subkategori?.NamaSubKategori}
</Badge>
<p className="text-gray-500 text-sm mt-2">
{item.date}
{new Date(item.created_at).toLocaleDateString(
"id-ID",
{
day: "numeric",
month: "long",
year: "numeric",
}
)}
</p>
<h3 className="text-md font-semibold mt-2 text-gray-900">
{item.title}
{item.JudulPost}
</h3>
<p className="text-sm text-gray-600 mt-2">
{item.description}
{item.DescPost.replace(/<[^>]*>/g, "").slice(
0,
160
)}
...
</p>
<Link href={route("undangan.show", item.SlugPost)}>
<Button
variant="link"
className="text-red-600 mt-2 pl-0"
@ -99,15 +92,74 @@ const CardUndangan = () => {
Baca Selengkapnya{" "}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</CardContent>
</Card>
))}
</div>
);
const handleTabChange = (value: string) => {
setActiveTab(value);
setVisibleItems(3); // Reset visible items when changing tabs
};
return (
<section className="container max-w-7xl py-8 px-6">
<div className="flex justify-center items-center w-full">
<Tabs
defaultValue="all"
className="w-full max-w-6xl"
onValueChange={handleTabChange}
>
<div className="flex justify-center">
<TabsList className="mb-6">
<TabsTrigger value="all">Semua</TabsTrigger>
{subcategories.map((subcat) => (
<TabsTrigger
key={subcat}
value={subcat as string}
>
{subcat}
</TabsTrigger>
))}
</TabsList>
</div>
<TabsContent value="all">
<PostGrid items={filterPosts(null)} />
{visibleItems < filterPosts(null).length && (
<div className="flex justify-center mt-8">
<Button variant="link" className="text-green-700">
<Button
variant="link"
className="text-green-700"
onClick={loadMore}
>
Lihat Lebih Banyak
</Button>
</div>
)}
</TabsContent>
{subcategories.map((subcat) => (
<TabsContent key={subcat} value={subcat as string}>
<PostGrid items={filterPosts(subcat as string)} />
{visibleItems <
filterPosts(subcat as string).length && (
<div className="flex justify-center mt-8">
<Button
variant="link"
className="text-green-700"
onClick={loadMore}
>
Lihat Lebih Banyak
</Button>
</div>
)}
</TabsContent>
))}
</Tabs>
</div>
</section>
);
};

View File

@ -17,13 +17,22 @@ export default function HeroSecond({ title }: HeroSecondProps) {
/>
{/* Overlay Content */}
<div className="absolute inset-0 flex flex-col items-center justify-center">
<h1 className="text-2xl md:text-5xl font-bold text-gray-800 mb-4 text-center md:text-right absolute md:right-24 top-1/2 -translate-y-1/2">
<div className="absolute inset-0 flex flex-col items-center justify-center md:flex-row md:items-center md:justify-between md:p-8">
<div className="hidden md:flex md:w-1/3 md:justify-center">
<img
src="/assets/dlh-logo.svg"
alt=""
className="md:max-w-[170px] ml-28"
/>
</div>
<div className="md:w-1/2 md:flex md:justify-center">
<h1 className="text-2xl md:text-5xl font-bold text-[#43b311] text-center">
{title}
</h1>
</div>
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,82 @@
import React from "react";
import { Link } from "@inertiajs/react";
import { DetailArtikelProps } from "./types";
export default function DetailArtikel({
article,
relatedArticles,
}: DetailArtikelProps) {
const getRouteByCategory = (kategori: string, slug: string) => {
const routes: { [key: string]: string } = {
Pengumuman: `/pengumuman/${slug}`,
Peraturan: `/peraturan/${slug}`,
Undangan: `/undangan/${slug}`,
};
return routes[kategori] || `/sekilasinfo/${slug}`;
};
return (
<div className="container flex flex-col md:flex-row gap-8 p-4 max-w-6xl mx-auto">
<div className="w-full md:w-7/12">
<div className="mb-4 flex flex-row gap-5 items-center">
<span className="bg-red-500 text-white px-2 py-1 text-sm rounded">
{article.kategori?.NamaKategori}
</span>
<div className="text-gray-500 text-sm ">
{new Date(article.created_at).toLocaleDateString(
"id-ID",
{
day: "numeric",
month: "long",
year: "numeric",
}
)}
</div>
</div>
<h1 className="text-2xl font-bold mb-6">{article.JudulPost}</h1>
<img
src={`/storage/${article.ImagePost}`}
alt={article.JudulPost}
className="w-full h-auto rounded-lg mb-6"
/>
<div
className="text-justify prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: article.DescPost }}
/>
</div>
<div className="w-full md:w-5/12">
<h1 className="text-xl font-bold mb-6">Informasi Lainnya</h1>
<div className="flex flex-col gap-6">
{relatedArticles.slice(0, 5).map((related) => (
<Link
key={related.PostId}
href={getRouteByCategory(
related.kategori?.NamaKategori || "",
related.SlugPost
)}
className="group"
>
<div className="flex flex-row gap-4">
<img
src={`/storage/${related.ImagePost}`}
alt={related.JudulPost}
className="w-24 h-24 object-cover rounded-lg"
/>
<div className="flex flex-col">
<span className="bg-red-500 text-white px-2 py-1 text-sm rounded w-fit">
{related.kategori?.NamaKategori}
</span>
<p className="font-medium group-hover:text-red-600 transition-colors mt-2">
{related.JudulPost}
</p>
</div>
</div>
</Link>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,26 @@
export interface SubKategori {
SubKategoriId: number;
NamaSubKategori: string;
}
export interface Kategori {
KategoriId: number;
NamaKategori: string;
}
export interface Post {
PostId: number;
JudulPost: string;
DescPost: string;
SlugPost: string;
ImagePost: string;
IsPublish: boolean;
created_at: string;
kategori?: Kategori;
subkategori?: SubKategori;
}
export interface DetailArtikelProps {
article: Post;
relatedArticles: Post[];
}

View File

@ -0,0 +1,86 @@
import React from "react";
import { Link } from "@inertiajs/react";
import { Post } from "@/components/DetailArtikel/types";
interface DetailSearchProps {
searchResults: Post[];
searchQuery: string;
}
export default function DetailSearch({
searchResults,
searchQuery,
}: DetailSearchProps) {
const getRouteByCategory = (kategori: string, slug: string) => {
const routes: { [key: string]: string } = {
Pengumuman: `/pengumuman/${slug}`,
Peraturan: `/peraturan/${slug}`,
Undangan: `/undangan/${slug}`,
};
return routes[kategori] || `/sekilasinfo/${slug}`;
};
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-2xl font-bold mb-2">Hasil Pencarian</h1>
<p className="text-gray-600">
Menampilkan hasil untuk "{searchQuery}" (
{searchResults.length} hasil)
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{searchResults.map((result) => (
<Link
key={result.PostId}
href={getRouteByCategory(
result.kategori?.NamaKategori || "",
result.SlugPost
)}
className="group"
>
<div className="border rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow h-full">
<img
src={`/storage/${result.ImagePost}`}
alt={result.JudulPost}
className="w-full h-48 object-cover"
/>
<div className="p-4">
<div className="flex items-center gap-2 mb-2">
<span className="bg-red-500 text-white px-2 py-1 text-xs rounded">
{result.kategori?.NamaKategori}
</span>
<span className="text-sm text-gray-500">
{new Date(
result.created_at
).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</span>
</div>
<h2 className="font-semibold mb-2 group-hover:text-red-600 transition-colors">
{result.JudulPost}
</h2>
<p className="text-sm text-gray-600 line-clamp-2">
{result.DescPost.replace(/<[^>]*>/g, "")}
</p>
</div>
</div>
</Link>
))}
</div>
{searchResults.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500">
Tidak ada hasil yang ditemukan
</p>
</div>
)}
</div>
);
}

View File

@ -22,7 +22,7 @@ const HeroSection = () => {
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4 text-center">
<TypeAnimation
sequence={[
"Status Ketaatan Lingkungan",
"Sistem Ketaatan Lingkungan",
1000,
"",
500,

View File

@ -6,7 +6,7 @@ import {
NavigationMenuItem,
} from "@/components/ui/navigation-menu";
import { Button } from "@/components/ui/button";
import { Link, router } from "@inertiajs/react";
import { Link, router, usePage } from "@inertiajs/react";
import {
Drawer,
DrawerClose,
@ -18,13 +18,22 @@ import {
import RunningText from "./RunningText";
import { useTheme } from "next-themes";
import SearchDialog from "./SearchDialog";
import { Post } from "../DetailArtikel/types";
import { PageProps as InertiaPageProps } from "@inertiajs/core";
interface NavItemsProps {
mobile?: boolean;
onClose?: () => void;
}
interface CustomPageProps extends InertiaPageProps {
runPosts?: Post[];
errors: Record<string, string>;
}
const Navbar = () => {
const { runPosts = [] } = usePage<CustomPageProps>().props;
// const runPosts = props.runPosts || [];
const [isScrolled, setIsScrolled] = useState(false);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// const [searchQuery, setSearchQuery] = useState("");
@ -103,7 +112,7 @@ const Navbar = () => {
return (
<div className="w-full">
<RunningText />
<RunningText posts={runPosts} />
<div
className={`w-full rounded-br-2xl rounded-bl-2xl text-white transition-all duration-300 ${
@ -184,7 +193,7 @@ const Navbar = () => {
</Drawer>
</div>
<Button
{/* <Button
variant="ghost"
onClick={toggleTheme}
className="p-3 bg-[#2d7448] hover:bg-[#16a34a] rounded-full text-white "
@ -194,7 +203,7 @@ const Navbar = () => {
) : (
<Sun className="h-6 w-6" />
)}
</Button>
</Button> */}
<div className="hidden md:block">
<Link

View File

@ -3,73 +3,84 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ArrowRight } from "lucide-react";
import { Link } from "@inertiajs/react";
const announcements = [
{
id: 1,
title_page: "Pengumuman",
alt_image: "Pengumuman",
date: "16 Januari 2025",
title: "Pelatihan & Sertifikasi Online Bidang Pengendalian Pencemaran Air Dan Udara",
description:
"Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan Tersebut Bekerjasama Dengan Lembaga P...",
image: "/assets/img1.jpg",
},
{
id: 2,
title_page: "Pengumuman",
alt_image: "Pengumuman",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
{
id: 3,
title_page: "Pengumuman",
alt_image: "Pengumuman",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
];
interface SubKategori {
SubKategoriId: number;
NamaSubKategori: string;
}
const AnnouncementSection = () => {
interface Kategori {
KategoriId: number;
NamaKategori: string;
}
interface Post {
PostId: number;
JudulPost: string;
DescPost: string;
SlugPost: string;
ImagePost: string;
IsPublish: boolean;
created_at: string;
kategori?: Kategori;
subkategori?: SubKategori;
}
interface CardPengumumanProps {
posts: Post[];
}
const AnnouncementSection = ({ posts }: CardPengumumanProps) => {
return (
<section className="container max-w-7xl py-8 px-6">
<Badge className="bg-black text-white hover:bg-green-600 cursor-pointer">
Informasi
</Badge>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-green-800">
Pengumuman
</h2>
<Link href="/pengumuman">
<Button variant="link" className="text-green-700">
Selengkapnya
</Button>
</Link>
</div>
{/* Highlight Announcement */}
{posts.length > 1 && (
<div className="grid grid-cols-1 gap-6 mb-8">
<Card className="p-4 flex flex-col md:flex-row items-start">
<img
src={announcements[0].image}
alt={announcements[0].alt_image}
src={`/storage/${posts[0].ImagePost}`}
alt={posts[0].JudulPost}
className="rounded-md w-full md:w-1/2"
/>
<CardContent className="p-4 w-full md:w-1/2">
<Badge className="bg-red-600 text-white">
{announcements[0].title_page}
{posts[0].kategori?.NamaKategori}
</Badge>
<p className="text-gray-500 text-sm mt-2">
{announcements[0].date}
{new Date(
posts[0].created_at
).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</p>
<h3 className="text-lg font-semibold mt-2 text-gray-900">
{announcements[0].title}
{posts[0].JudulPost}
</h3>
<p className="text-sm text-gray-600 mt-2">
{announcements[0].description}
{posts[0].DescPost.replace(
/<[^>]*>/g,
""
).slice(0, 160)}
...
</p>
<Link href={`/pengumuman/${posts[0].SlugPost}`}>
<Button
variant="link"
className="text-red-600 mt-2 pl-0"
@ -77,32 +88,46 @@ const AnnouncementSection = () => {
Baca Selengkapnya{" "}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</CardContent>
</Card>
</div>
)}
{/* List of Announcements */}
{posts.length > 1 && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{announcements.map((item) => (
<Card key={item.id} className="p-4">
{posts.slice(1).map((post) => (
<Card key={post.PostId} className="p-4">
<img
src={item.image}
alt={item.alt_image}
src={`/storage/${post.ImagePost}`}
alt={post.JudulPost}
className="rounded-md"
/>
<CardContent className="p-4">
<Badge className="bg-red-600 text-white">
{item.title_page}
{post.kategori?.NamaKategori}
</Badge>
<p className="text-gray-500 text-sm mt-2">
{item.date}
{new Date(
post.created_at
).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</p>
<h3 className="text-md font-semibold mt-2 text-gray-900">
{item.title}
{post.JudulPost}
</h3>
<p className="text-sm text-gray-600 mt-2">
{item.description}
{post.DescPost.replace(
/<[^>]*>/g,
""
).slice(0, 160)}
...
</p>
<Link href={`/pengumuman/${post.SlugPost}`}>
<Button
variant="link"
className="text-red-600 mt-2 pl-0"
@ -110,10 +135,12 @@ const AnnouncementSection = () => {
Baca Selengkapnya{" "}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</CardContent>
</Card>
))}
</div>
)}
</section>
);
};

View File

@ -3,31 +3,35 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ArrowRight, FileText } from "lucide-react";
import { Link } from "@inertiajs/react";
const regulations = [
{
id: 1,
title_page: "Peraturan",
alt_image: "Peraturan",
date: "16 Januari 2025",
title: "Pelatihan & Sertifikasi Online Bidang Pengendalian Pencemaran Air Dan Udara",
description:
"Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan Tersebut Bekerjasama Dengan Lembaga P...",
image: "/assets/img1.jpg",
},
{
id: 2,
title_page: "Peraturan",
alt_image: "Peraturan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
];
interface SubKategori {
SubKategoriId: number;
NamaSubKategori: string;
}
const RegulationSection = () => {
interface Kategori {
KategoriId: number;
NamaKategori: string;
}
interface Post {
PostId: number;
JudulPost: string;
DescPost: string;
SlugPost: string;
ImagePost: string;
IsPublish: boolean;
created_at: string;
kategori?: Kategori;
subkategori?: SubKategori;
}
interface CardPeraturanProps {
peraturan: Post[];
}
const RegulationSection = ({ peraturan }: CardPeraturanProps) => {
return (
<section className="relative w-full py-8 px-6">
{/* Background Green Box */}
@ -43,38 +47,51 @@ const RegulationSection = () => {
Informasi
</Badge>
<h2 className="text-2xl font-bold">Peraturan</h2>
<p className="text-sm text-[#94bb98] mt-2 text-center md:text-left">
Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan
Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan
Tersebut Bekerjasama Dengan Lembaga P...
<p className="text-sm text-[#94bb98] mt-2 text-center md:text-left pr-3">
Aplikasi Sistem Ketaatan Lingkungan memastikan kepatuhan
terhadap perizinan, pengelolaan limbah, emisi, serta
pengawasan lingkungan.
</p>
<Link href="/peraturan">
<Button variant="link" className="text-white mt-4 pl-0">
Selengkapnya
</Button>
</Link>
</div>
{/* List of Regulations */}
<div className="col-span-2 grid grid-cols-1 md:grid-cols-2 gap-6 relative z-10">
{regulations.map((item) => (
<Card key={item.id} className="md:p-4">
{peraturan.map((item) => (
<Card key={item.PostId} className="md:p-4">
<img
src={item.image}
alt={item.alt_image}
src={`/storage/${item.ImagePost}`}
alt={item.JudulPost}
className="rounded-md"
/>
<CardContent className="p-4">
<Badge className="bg-red-600 text-white">
{item.title_page}
{item.kategori?.NamaKategori}
</Badge>
<p className="text-gray-500 text-sm mt-2">
{item.date}
{new Date(
item.created_at
).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</p>
<h3 className="text-md font-semibold mt-2 text-gray-900">
{item.title}
{item.JudulPost}
</h3>
<p className="text-sm text-gray-600 mt-2">
{item.description}
{item.DescPost.replace(
/<[^>]*>/g,
""
).slice(0, 160)}
...
</p>
<Link href={`/peraturan/${item.SlugPost}`}>
<Button
variant="link"
className="text-red-600 mt-2 pl-0"
@ -82,6 +99,7 @@ const RegulationSection = () => {
Baca Selengkapnya{" "}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</CardContent>
</Card>
))}

View File

@ -1,31 +1,26 @@
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { X, ArrowLeft, ArrowRight } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { usePage } from "@inertiajs/react";
import { Link } from "@inertiajs/react";
const PopupModal = ({ onClose }: { onClose: () => void }) => {
const slides = [
{
title: "Pelatihan dan Sertifikasi Online Bidang Pengendalian Pencemaran Udara",
image: "/assets/popup-1.jpeg",
description:
"Kegiatan ini diselenggarakan oleh DLH Provinsi DKI Jakarta bekerja sama dengan LSP Tersertifikasi BNSP.",
},
{
title: "Workshop Peningkatan Kapasitas Pengendalian Pencemaran Udara",
image: "/assets/popup-2.jpeg",
description:
"Workshop ini bertujuan untuk meningkatkan pengetahuan dan keterampilan di bidang lingkungan hidup. Workshop ini bertujuan untuk meningkatkan pengetahuan dan keterampilan di bidang lingkungan hidup.",
},
{
title: "Sosialisasi Program Ramah Lingkungan",
image: "/assets/popup-2.jpeg",
description:
"DLH DKI Jakarta mengajak masyarakat untuk lebih peduli terhadap lingkungan sekitar.",
},
];
const { sekilasInfo } = usePage().props as any;
const [isVisible, setIsVisible] = useState(true);
const [currentSlide, setCurrentSlide] = useState(0);
const progressValue = ((currentSlide + 1) / sekilasInfo.length) * 100;
const slides = sekilasInfo.map((info: any) => ({
title: info.JudulPost,
image: `/storage/${info.ImagePost}`,
description:
info.DescPost.replace(/<[^>]*>/g, "").slice(0, 160) + "...",
slug: info.SlugPost,
}));
const nextSlide = () => {
setCurrentSlide((prev) => (prev + 1) % slides.length);
};
@ -34,13 +29,63 @@ const PopupModal = ({ onClose }: { onClose: () => void }) => {
setCurrentSlide((prev) => (prev - 1 + slides.length) % slides.length);
};
const handleClose = () => {
setIsVisible(false);
setTimeout(onClose, 500);
};
return (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50 container md:max-w-none">
<div className="bg-white p-6 rounded-lg relative md:max-w-2xl mx-auto w-full h-50 md:h-5/6 flex flex-col justify-between md:justify-around">
<button className="absolute top-2 right-2" onClick={onClose}>
<AnimatePresence mode="wait">
{isVisible && sekilasInfo.length > 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: {
duration: 0.3,
delay: 0.1,
},
}}
exit={{
opacity: 0,
transition: {
duration: 0.3,
},
}}
className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50 container md:max-w-none"
>
<motion.div
initial={{ opacity: 0, y: -50 }}
animate={{
opacity: 1,
y: 0,
transition: {
type: "spring",
damping: 70,
stiffness: 500,
delay: 0.2,
duration: 0.5,
},
}}
exit={{
opacity: 0,
y: 50,
transition: {
type: "spring",
damping: 25,
stiffness: 300,
duration: 0.4,
},
}}
className="bg-white p-6 rounded-lg relative md:max-w-2xl mx-auto w-full h-50 md:h-5/6 flex flex-col justify-between md:justify-around"
>
<button
className="absolute top-2 right-2"
onClick={handleClose}
>
<X className="w-6 h-6 text-gray-700" />
</button>
<h2 className="md:text-xl text-md font-bold text-center mb-4">
<h2 className="md:text-md text-base font-bold text-center mb-4">
{slides[currentSlide].title}
</h2>
<div className="relative">
@ -62,19 +107,31 @@ const PopupModal = ({ onClose }: { onClose: () => void }) => {
<ArrowRight className="w-6 h-6 text-gray-700" />
</button>
</div>
{/* <h2 className="text-xl font-bold text-center mb-4">
{slides[currentSlide].title}
</h2> */}
<div className="w-full mt-4">
<Progress
value={progressValue}
className="w-full"
/>
<div className="text-sm text-gray-500 text-center mt-2">
{currentSlide + 1} / {slides.length}
</div>
</div>
<p className="text-sm text-gray-600 text-center mb-4 overflow-hidden text-ellipsis whitespace-nowrap">
{slides[currentSlide].description}
</p>
<div className="flex justify-center">
<Link
href={`/sekilasinfo/${slides[currentSlide].slug}`}
>
<Button className="bg-green-800 text-white px-6 py-2 rounded-lg">
Selengkapnya
</Button>
</Link>
</div>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};

View File

@ -1,43 +1,106 @@
import React, { useState, useEffect } from "react";
import { Megaphone } from "lucide-react";
import { Link } from "@inertiajs/react";
import { Post } from "@/components/DetailArtikel/types";
const messages = [
"Peraturan Menteri Lingkungan Hidup dan Kehutanan No. 14 Tahun 2024 Tentang Penyelenggaraan Pengawasan dan Sanksi Administratif Lingkungan Hidup",
"Workshop Peningkatan Kapasitas Pengendalian Pencemaran Udara akan diselenggarakan pada bulan depan.",
"DLH DKI Jakarta mengajak masyarakat untuk berpartisipasi dalam program lingkungan ramah sampah.",
];
interface RunningTextProps {
posts: Post[];
}
export default function RunningText() {
export default function RunningText({ posts }: RunningTextProps) {
const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
const [isVisible, setIsVisible] = useState(true);
const [isPaused, setIsPaused] = useState(false);
const getLatestPostsByCategory = () => {
const groupedPosts = posts.reduce((acc, post) => {
const category = post.kategori?.NamaKategori || "Uncategorized";
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(post);
return acc;
}, {} as Record<string, Post[]>);
// Get latest 2 posts from each category and flatten
return Object.values(groupedPosts)
.map((categoryPosts) =>
categoryPosts
.sort(
(a, b) =>
new Date(b.created_at).getTime() -
new Date(a.created_at).getTime()
)
.slice(0, 2)
)
.flat();
};
const messages = getLatestPostsByCategory();
useEffect(() => {
if (isPaused || messages.length === 0) return;
const interval = setInterval(() => {
setIsVisible(false); // Hide current message
setIsVisible(false);
setTimeout(() => {
setCurrentMessageIndex(
(prevIndex) => (prevIndex + 1) % messages.length
);
setIsVisible(true); // Show new message
}, 500); // Wait for fade out animation
}, 3000); // Change message every 3 seconds
setIsVisible(true);
}, 500);
}, 3000);
return () => clearInterval(interval);
}, []);
}, [isPaused, messages.length]);
const getRouteByCategory = (kategori: string, slug: string) => {
const routes: { [key: string]: string } = {
Pengumuman: `/pengumuman/${slug}`,
Peraturan: `/peraturan/${slug}`,
Undangan: `/undangan/${slug}`,
};
return routes[kategori] || `/sekilasinfo/${slug}`;
};
if (messages.length === 0) return null;
return (
<div className="w-full bg-green-100 p-2 flex overflow-hidden">
<div
className="w-full bg-green-100 p-2 flex overflow-hidden"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
>
<div className="flex container max-w-7xl px-8 relative">
<Megaphone className="w-5 h-5 mr-2 text-black flex-shrink-0" />
<div className="overflow-hidden relative h-[1.5rem]">
<Link
href={getRouteByCategory(
messages[currentMessageIndex].kategori
?.NamaKategori || "",
messages[currentMessageIndex].SlugPost
)}
className="hover:text-green-700 transition-colors"
>
<p
className={`text-sm text-black transition-opacity duration-500 ease-in-out ${
isVisible ? "opacity-100" : "opacity-0"
}`}
>
{messages[currentMessageIndex]}
<span className="font-semibold mr-2 bg-green-600 p-1 text-white text-xs rounded">
{
messages[currentMessageIndex].kategori
?.NamaKategori
}
</span>
<span>
{" "}
{messages[currentMessageIndex].JudulPost}
</span>
</p>
</Link>
</div>
</div>
</div>

View File

@ -17,7 +17,10 @@ const SearchDialog: React.FC = () => {
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (searchQuery.trim()) {
router.visit(`/search/${encodeURIComponent(searchQuery)}`);
router.visit(`/search?q=${encodeURIComponent(searchQuery)}`, {
preserveState: true,
preserveScroll: true,
});
setSearchQuery(""); // Clear input setelah search
}
};
@ -56,14 +59,14 @@ const SearchDialog: React.FC = () => {
<Search className="h-5 w-5" />
</Button>
</form>
<DialogClose asChild>
{/* <DialogClose asChild>
<Button
variant="ghost"
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700"
>
</Button>
</DialogClose>
</DialogClose> */}
</DialogContent>
</Dialog>
);

View File

@ -21,25 +21,19 @@ export default function Tentang() {
</Badge>
<h2 className="text-3xl font-bold text-green-600">
Status Ketaatan Lingkungan
Sistem Ketaatan Lingkungan
<br />
(SKL)
</h2>
<p className="text-gray-600 leading-relaxed max-w-2xl">
Lorem Ipsum Is Simply Dummy Text Of The Printing
And Typesetting Industry. Lorem Ipsum Has Been
The Industry's Standard Dummy Text Ever Since
The 1500s, When An Unknown Printer Took A Galley
Of Type And Scrambled It To Make A Type Specimen
Book. It Has Survived Not Only Five Centuries,
But Also The Leap Into Electronic Typesetting,
Remaining Essentially Unchanged. It Was
Popularised In The 1960s With The Release Of
Letraset Sheets Containing Lorem Ipsum Passages,
And More Recently With Desktop Publishing
Software Like Aldus PageMaker Including Versions
Of Lorem Ipsum.
SKL adalah Sistem Ketaatan Lingkungan yang
dinaungi oleh Dinas Lingkungan Hidup Provinsi
DKI Jakarta dengan menyediakan sistem yang dapat
memberikan pelayanan dalam pelaporan, analisa
dan evaluasi pengelolaan lingkungan dilakukan
oleh perusahaan atau kegiatan usaha yang berada
di Provinsi DKI Jakarta.
</p>
</div>
</div>

View File

@ -3,41 +3,35 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { ArrowRight } from "lucide-react";
import { Link } from "@inertiajs/react";
const undangans = [
{
id: 1,
title_page: "Undangan",
alt_image: "Undangan",
date: "16 Januari 2025",
title: "Pelatihan & Sertifikasi Online Bidang Pengendalian Pencemaran Air Dan Udara",
description:
"Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan Tersebut Bekerjasama Dengan Lembaga P...",
image: "/assets/img1.jpg",
},
{
id: 2,
title_page: "Undangan",
alt_image: "Undangan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
{
id: 3,
title_page: "Undangan",
alt_image: "Undangan",
date: "12 Desember 2024",
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
description:
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
image: "/assets/img1.jpg",
},
];
interface SubKategori {
SubKategoriId: number;
NamaSubKategori: string;
}
const UndanganSection = () => {
interface Kategori {
KategoriId: number;
NamaKategori: string;
}
interface Post {
PostId: number;
JudulPost: string;
DescPost: string;
SlugPost: string;
ImagePost: string;
IsPublish: boolean;
created_at: string;
kategori?: Kategori;
subkategori?: SubKategori;
}
interface CardUndanganProps {
undangan: Post[];
}
const UndanganSection = ({ undangan }: CardUndanganProps) => {
return (
<section className="container max-w-7xl py-8 px-6">
<div className="flex items-center mb-6 justify-between">
@ -49,33 +43,50 @@ const UndanganSection = () => {
Undangan
</h2>
</div>
<Button variant="link" className="text-green-700 mt-auto pl-0">
<Link href="/undangan">
<Button
variant="link"
className="text-green-700 mt-auto pl-0"
>
Selengkapnya
</Button>
</Link>
</div>
{/* List of Announcements */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{undangans.map((item) => (
<Card key={item.id} className="md:p-4">
{undangan.map((item) => (
<Card key={item.PostId} className="md:p-4">
<img
src={item.image}
alt={item.alt_image}
src={`/storage/${item.ImagePost}`}
alt={item.JudulPost}
className="rounded-md"
/>
<CardContent className="p-4">
<Badge className="bg-red-600 text-white">
{item.title_page}
{item.kategori?.NamaKategori}
</Badge>
<p className="text-gray-500 text-sm mt-2">
{item.date}
{new Date(item.created_at).toLocaleDateString(
"id-ID",
{
day: "numeric",
month: "long",
year: "numeric",
}
)}
</p>
<h3 className="text-md font-semibold mt-2 text-gray-900">
{item.title}
{item.JudulPost}
</h3>
<p className="text-sm text-gray-600 mt-2">
{item.description}
{item.DescPost.replace(/<[^>]*>/g, "").slice(
0,
160
)}
...
</p>
<Link href={`/undangan/${item.SlugPost}`}>
<Button
variant="link"
className="text-red-600 mt-2 pl-0"
@ -83,6 +94,7 @@ const UndanganSection = () => {
Baca Selengkapnya{" "}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</CardContent>
</Card>
))}

View File

@ -2,11 +2,22 @@
import * as React from "react";
import {
Archive,
Bold,
Bolt,
BookCheck,
BookMarked,
Command,
DatabaseBackup,
FileKey2,
Flag,
FolderArchive,
Frame,
GalleryVertical,
Home,
LifeBuoy,
NotepadText,
Scale,
Send,
SquareTerminal,
} from "lucide-react";
@ -38,6 +49,64 @@ const data = {
url: "/dashboard",
icon: Home,
},
{
title: "Pelaporan",
url: "/admin/pelaporan",
icon: BookMarked,
},
{
title: "Verifikasi Pelaporan",
url: "/admin/verifikasi",
icon: BookCheck,
},
{
title: "Penegakan Hukum",
url: "/admin/hukum",
icon: Scale,
},
{
title: "Perizinan Lingkungan",
url: "/admin/perizinan-lingkungan",
icon: FileKey2,
},
{
title: "History Perusahaan",
url: "/admin/history-perusahaan",
icon: GalleryVertical,
},
{
title: "Post",
url: "/admin/post",
icon: Frame,
},
{
title: "Reporting",
url: "#",
icon: Flag,
isActive: true,
items: [
{
title: "Rekapitulasi",
url: "/admin/rekap",
},
{
title: "SKL & SPL",
url: "/admin/skl-spl",
},
{
title: "Daftar Perusahaan",
url: "/admin/daftar-perusahaan",
},
{
title: "SKL",
url: "/admin/skl",
},
{
title: "SPL",
url: "/admin/spl",
},
],
},
{
title: "Data Master",
url: "#",
@ -52,28 +121,69 @@ const data = {
title: "Sub Kategori Post",
url: "/admin/subkategori",
},
// {
// title: "Menu",
// url: "/menus",
// },
{
title: "Dinas LH",
url: "/admin/verifikator",
},
{
title: "Jenis Kegiatan",
url: "/admin/jeniskegiatan",
},
{
title: "Jenis Dokumen Izin",
url: "/admin/jenisdokil",
},
{
title: "Perusahaan",
url: "/admin/perusahaan",
},
{
title: "History Kegiatan",
url: "/admin/historykegiatan",
},
{
title: "Jenis Sanksi",
url: "/admin/jenissanksi",
},
],
},
{
title: "Post",
url: "/admin/post",
icon: Frame,
title: "Pengaturan",
url: "#",
icon: Bolt,
isActive: true,
items: [
{
title: "Pengguna",
url: "/admin/pengguna",
},
{
title: "Role",
url: "/admin/role",
},
],
},
],
navSecondary: [
{
title: "Support",
url: "#",
title: "Tentang",
url: "/admin/tentang",
icon: LifeBuoy,
},
{
title: "Feedback",
title: "Catatan Sistem",
url: "#",
icon: Send,
icon: NotepadText,
},
{
title: "Backup",
url: "#",
icon: Archive,
},
{
title: "Restore",
url: "#",
icon: DatabaseBackup,
},
],
};

View File

@ -44,11 +44,11 @@ const AppearanceDropdown = () => {
return (
<>
<div className="flex flex-row items-center justify-center">
<span className="text-sm border-r px-5 items-center text-right">
<span className="text-sm px-5 items-center text-right">
{formattedDate} <br />
{formattedTime}
</span>
<div className="px-5">
{/* <div className="px-5">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@ -74,7 +74,7 @@ const AppearanceDropdown = () => {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div> */}
</div>
</>
);

View File

@ -46,10 +46,10 @@ export function NavMain({
>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip={item.title}>
<a href={item.url}>
<Link href={item.url}>
<item.icon />
<div>{item.title}</div>
</a>
</Link>
</SidebarMenuButton>
{item.items?.length ? (
<>

View File

@ -8,6 +8,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { Link } from "@inertiajs/react";
export function NavSecondary({
items,
@ -26,10 +27,10 @@ export function NavSecondary({
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild size="sm">
<a href={item.url}>
<Link href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -0,0 +1,209 @@
"use client";
import * as React from "react";
import {
ColumnDef,
SortingState,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
searchKey: string;
searchPlaceholder?: string;
}
export function DataTable<TData, TValue>({
columns,
data,
searchKey,
searchPlaceholder = "Filter records...",
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] =
React.useState<ColumnFiltersState>([]);
const [rowSelection, setRowSelection] = React.useState({});
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
rowSelection,
},
});
return (
<div>
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 py-4">
<Input
placeholder={searchPlaceholder}
value={
(table
.getColumn(searchKey)
?.getFilterValue() as string) ?? ""
}
onChange={(event) =>
table
.getColumn(searchKey)
?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
<div className="flex items-center gap-2">
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue
placeholder={
table.getState().pagination.pageSize
}
/>
</SelectTrigger>
<SelectContent side="top">
{[5, 10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem
key={pageSize}
value={`${pageSize}`}
>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Wrap table in a responsive container */}
<div className="relative w-full overflow-auto">
<div className="rounded-md border">
<div className="w-full overflow-x-auto">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
className="whitespace-nowrap"
>
{header.isPlaceholder
? null
: flexRender(
header.column
.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={
row.getIsSelected() &&
"selected"
}
>
{row
.getVisibleCells()
.map((cell) => (
<TableCell
key={cell.id}
className="whitespace-nowrap"
>
{flexRender(
cell.column
.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 py-4">
<div className="text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
"border-b transition-colors data-[state=selected]:bg-muted",
className
)}
{...props}

View File

@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -27,7 +27,7 @@ const toastVariants = cva(
{
variants: {
variant: {
default: "border bg-background text-foreground",
default: "border bg-green-100 text-green-800",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},

View File

@ -12,7 +12,7 @@ export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider duration={2000} swipeDirection="right">
<ToastProvider duration={3000} swipeDirection="right">
{toasts.map(function ({
id,
title,

View File

@ -25,7 +25,7 @@ export default function AuthenticatedLayout({
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<SidebarInset className="overflow-x-hidden w-full">
<header className="sticky top-0 bg-background flex h-16 shrink-0 items-center gap-2 justify-between p-4 border-b md:border-none md:rounded-xl">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
@ -46,8 +46,10 @@ export default function AuthenticatedLayout({
</div>
</header>
<main className="p-4 md:pt-0 h-full">{children}</main>
<footer className="text-sm p-4">
<main className="p-4 md:pt-0 h-full w-full max-w-full overflow-x-hidden">
{children}
</main>
<footer className="text-sm p-4 text-center bg-background border-t">
© Copyright {new Date().getFullYear()} Bidang Tata
Lingkungan dan Kebersihan - Dinas Lingkungan Hidup Provinsi
DKI Jakarta

View File

@ -4,15 +4,21 @@ import Footer from "@/components/Partials/Footer";
import HeroSecond from "@/components/Card/HeroSecond";
import CardPengumuman from "@/components/Card/CardPengumuman";
import { Head } from "@inertiajs/react";
import { usePage } from "@inertiajs/react";
import Guest from "@/layouts/guest-layout";
export default function Pengumuman() {
const { posts } = usePage().props as any;
return (
<>
<Guest>
<Head title="Pengumuman" />
<Navbar />
<HeroSecond title="Pengumuman" />
<CardPengumuman posts={[]} />
<CardPengumuman posts={posts} />
<Footer />
</Guest>
</>
);
}

View File

@ -3,15 +3,16 @@ import Navbar from "@/components/Partials/Navbar";
import Footer from "@/components/Partials/Footer";
import HeroSecond from "@/components/Card/HeroSecond";
import CardPeraturan from "@/components/Card/CardPeraturan";
import { Head } from "@inertiajs/react";
import { Head, usePage } from "@inertiajs/react";
export default function Peraturan() {
const { posts } = usePage().props as any;
return (
<>
<Head title="Peraturan" />
<Navbar />
<HeroSecond title="Peraturan" />
<CardPeraturan />
<CardPeraturan posts={posts} />
<Footer />
</>
);

View File

@ -0,0 +1,26 @@
import React from "react";
import { Head } from "@inertiajs/react";
import DetailSearch from "@/components/DetailSearch/DetailSearch";
import Navbar from "@/components/Partials/Navbar";
import Footer from "@/components/Partials/Footer";
import { Post } from "@/components/DetailArtikel/types";
import Guest from "@/layouts/guest-layout";
interface SearchProps {
searchResults: Post[];
searchQuery: string;
}
export default function Search({ searchResults, searchQuery }: SearchProps) {
return (
<>
<Head title={`Pencarian: ${searchQuery}`} />
<Navbar />
<DetailSearch
searchResults={searchResults}
searchQuery={searchQuery}
/>
<Footer />
</>
);
}

View File

@ -3,15 +3,16 @@ import Navbar from "@/components/Partials/Navbar";
import Footer from "@/components/Partials/Footer";
import HeroSecond from "@/components/Card/HeroSecond";
import CardUndangan from "@/components/Card/CardUndangan";
import { Head } from "@inertiajs/react";
import { Head, usePage } from "@inertiajs/react";
export default function Undangan() {
const { posts } = usePage().props as any;
return (
<>
<Head title="Undangan" />
<Navbar />
<HeroSecond title="Undangan" />
<CardUndangan />
<CardUndangan posts={posts} />
<Footer />
</>
);

View File

@ -0,0 +1,296 @@
import React, { useEffect, useState } from "react";
import { useForm } from "@inertiajs/react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Table,
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell,
} from "@/components/ui/table";
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 axios from "axios";
interface Hukum {
HukumId: number;
PerusahaanId: number | null;
JenisSanksiId: number | null;
SanksiNumber: string;
SanksiDate: string;
SanksiFile: string;
StatusPenaatan: number;
PenaatanNumber: string;
PenaatanDate: string;
PenaatanFile: string;
IsDeleted: number;
perusahaan: { nama: string } | null;
jenisSanksi: { nama: string } | null;
}
const ITEMS_PER_PAGE = 5;
export default function HukumIndex() {
const { toast } = useToast();
const [dataHukum, setDataHukum] = useState<Hukum[]>([]);
const [search, setSearch] = useState("");
const [filteredHukum, setFilteredHukum] = useState<Hukum[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editing, setEditing] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<Hukum | null>(null);
const {
data,
setData,
post,
put,
delete: destroy,
reset,
} = useForm<Hukum>({
HukumId: 0,
PerusahaanId: null,
JenisSanksiId: null,
SanksiNumber: "",
SanksiDate: "",
SanksiFile: "",
StatusPenaatan: 1,
PenaatanNumber: "",
PenaatanDate: "",
PenaatanFile: "",
IsDeleted: 0,
perusahaan: null,
jenisSanksi: null,
});
useEffect(() => {
axios
.get("/admin/hukum")
.then((response) => {
if (response.data && response.data.data) {
setDataHukum(response.data.data);
setFilteredHukum(response.data.data);
} else {
setDataHukum([]);
setFilteredHukum([]);
}
})
.catch((error) => {
console.error("Error fetching data:", error);
setDataHukum([]);
setFilteredHukum([]);
});
}, []);
// Filter Data
useEffect(() => {
let filtered = dataHukum;
if (search) {
filtered = filtered.filter((hukum) =>
hukum.SanksiNumber.toLowerCase().includes(search.toLowerCase())
);
}
setFilteredHukum(filtered);
setCurrentPage(1);
}, [dataHukum, search]);
// Pagination
const totalPages = filteredHukum?.length
? Math.ceil(filteredHukum.length / ITEMS_PER_PAGE)
: 1;
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const currentItems = filteredHukum.slice(startIndex, endIndex);
// Handle Search
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
// Handle Page Change
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
// Handle Form Submission
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const url = editing ? `/admin/hukum/${data.HukumId}` : "/admin/hukum";
const requestMethod = editing ? put : post;
requestMethod(url, {
onSuccess: () => {
toast({
title: "Berhasil",
description: editing
? "Data berhasil diperbarui"
: "Data berhasil ditambahkan",
variant: "default",
});
setIsModalOpen(false);
reset();
setEditing(false);
},
onError: () => {
toast({
title: "Gagal",
description: "Terjadi kesalahan saat menyimpan data",
variant: "destructive",
});
},
});
};
// Handle Edit
const handleEdit = (hukum: Hukum) => {
setData({ ...hukum });
setEditing(true);
setIsModalOpen(true);
};
// Handle Delete
const handleDelete = () => {
if (deleteConfirm) {
destroy(`/admin/hukum/${deleteConfirm.HukumId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Data berhasil dihapus",
variant: "default",
});
setDeleteConfirm(null);
},
onError: () => {
toast({
title: "Gagal",
description: "Terjadi kesalahan saat menghapus data",
variant: "destructive",
});
},
});
}
};
return (
<AuthenticatedLayout header="Penegakan Hukum">
<Head title="Penegakan Hukum" />
<div className="container mx-auto p-4">
<Card className="shadow-lg">
<CardHeader>
<div className="flex justify-between">
<Input
type="text"
placeholder="Cari Sanksi..."
value={search}
onChange={handleSearch}
/>
<Button
onClick={() => {
setEditing(false);
setIsModalOpen(true);
}}
>
<Plus className="h-4 w-4" />
Tambah
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>No</TableHead>
<TableHead>Nama Perusahaan</TableHead>
<TableHead>Jenis Sanksi</TableHead>
<TableHead>Nomor SK Sanksi</TableHead>
<TableHead>Tanggal Sanksi</TableHead>
<TableHead>SK Sanksi</TableHead>
<TableHead>Status Penaatan</TableHead>
<TableHead>Nomor Penaatan</TableHead>
<TableHead>Tanggal Penaatan</TableHead>
<TableHead>SK Penaatan</TableHead>
<TableHead>Aksi</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{currentItems.map((hukum, index) => (
<TableRow key={hukum.HukumId}>
<TableCell>
{startIndex + index + 1}
</TableCell>
<TableCell>
{hukum.perusahaan?.nama}
</TableCell>
<TableCell>
{hukum.jenisSanksi?.nama}
</TableCell>
<TableCell>
{hukum.SanksiNumber}
</TableCell>
<TableCell>
{hukum.SanksiDate}
</TableCell>
<TableCell>
{hukum.SanksiFile}
</TableCell>
<TableCell>
{hukum.StatusPenaatan}
</TableCell>
<TableCell>
{hukum.PenaatanNumber}
</TableCell>
<TableCell>
{hukum.PenaatanDate}
</TableCell>
<TableCell>
{hukum.PenaatanFile}
</TableCell>
<TableCell>
<Button
onClick={() =>
handleEdit(hukum)
}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
onClick={() =>
setDeleteConfirm(hukum)
}
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
<Toaster />
</AuthenticatedLayout>
);
}

View File

@ -0,0 +1,398 @@
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 JenisDokIL {
JenisDokILId: number | null;
KodeJenisDokIL: string;
NamaJenisDokIL: string;
}
const ITEMS_PER_PAGE = 5;
export default function JenisDokILIndex({
jenisdokil,
}: PageProps<{ jenisdokil: JenisDokIL[] }>) {
const {
data,
setData,
post,
put,
delete: destroy,
reset,
} = useForm<JenisDokIL>({
JenisDokILId: null,
KodeJenisDokIL: "",
NamaJenisDokIL: "",
});
const { toast } = useToast();
const [editing, setEditing] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<JenisDokIL | null>(null);
const [search, setSearch] = useState("");
const [filteredJenisDokIL, setFilteredJenisDokIL] = useState(jenisdokil);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
let filtered = jenisdokil;
if (search) {
filtered = filtered.filter(
(item) =>
item.KodeJenisDokIL.toLowerCase().includes(
search.toLowerCase()
) ||
item.NamaJenisDokIL.toLowerCase().includes(
search.toLowerCase()
)
);
}
setFilteredJenisDokIL(filtered);
setCurrentPage(1);
}, [jenisdokil, search]);
const totalPages = Math.ceil(filteredJenisDokIL.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const currentItems = filteredJenisDokIL.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/jenisdokil/${data.JenisDokILId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Data jenis dokumen berhasil diperbarui",
variant: "default",
});
setIsModalOpen(false);
reset();
setEditing(false);
},
onError: () => {
toast({
title: "Gagal",
description: "Terjadi kesalahan saat memperbarui data",
variant: "destructive",
});
},
});
} else {
post("/admin/jenisdokil", {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Data jenis dokumen berhasil ditambahkan",
variant: "default",
});
setIsModalOpen(false);
reset();
},
onError: () => {
toast({
title: "Gagal",
description: "Terjadi kesalahan saat menambah data",
variant: "destructive",
});
},
});
}
};
const handleEdit = (jenisdokil: JenisDokIL) => {
setData({ ...jenisdokil });
setEditing(true);
setIsModalOpen(true);
};
const handleDelete = () => {
if (deleteConfirm) {
destroy(`/admin/jenisdokil/${deleteConfirm.JenisDokILId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Data jenis dokumen berhasil dihapus",
variant: "default",
});
setDeleteConfirm(null);
},
onError: () => {
toast({
title: "Gagal",
description: "Terjadi kesalahan saat menghapus data",
variant: "destructive",
});
setDeleteConfirm(null);
},
});
}
};
return (
<AuthenticatedLayout header={"Data Jenis Dokumen IL"}>
<Head title="Data Jenis Dokumen IL" />
<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 Jenis Dokumen IL..."
value={search}
onChange={handleSearch}
className="pl-10 pr-4 w-full"
/>
</div>
<Dialog
open={isModalOpen}
onOpenChange={setIsModalOpen}
>
<DialogTrigger asChild>
<Button
onClick={() => {
setEditing(false);
setIsModalOpen(true);
}}
>
<Plus className="h-4 w-4 mr-2" />
Tambah Jenis Dokumen
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editing
? "Edit Jenis Dokumen"
: "Tambah Jenis Dokumen"}
</DialogTitle>
</DialogHeader>
<form
onSubmit={handleSubmit}
className="space-y-4"
>
<div className="space-y-2">
<label>
Kode Jenis Dokumen
</label>
<Input
value={data.KodeJenisDokIL}
onChange={(e) =>
setData(
"KodeJenisDokIL",
e.target.value
)
}
placeholder="Masukkan kode jenis dokumen"
/>
</div>
<div className="space-y-2">
<label>
Nama Jenis Dokumen
</label>
<Input
value={data.NamaJenisDokIL}
onChange={(e) =>
setData(
"NamaJenisDokIL",
e.target.value
)
}
placeholder="Masukkan nama jenis dokumen"
/>
</div>
<DialogFooter>
<Button type="submit">
{editing
? "Simpan"
: "Tambah"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>No</TableHead>
<TableHead>Kode</TableHead>
<TableHead>Nama Jenis Dokumen</TableHead>
<TableHead>Aksi</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{currentItems.map((item, index) => (
<TableRow key={item.JenisDokILId}>
<TableCell>
{startIndex + index + 1}
</TableCell>
<TableCell>
{item.KodeJenisDokIL}
</TableCell>
<TableCell>
{item.NamaJenisDokIL}
</TableCell>
<TableCell className="flex gap-2">
<Button
variant="outline"
onClick={() => handleEdit(item)}
>
<Pencil className="h-4 w-4 mr-2" />
Edit
</Button>
<Button
variant="destructive"
onClick={() =>
setDeleteConfirm(item)
}
>
<Trash2 className="h-4 w-4 mr-2" />
Hapus
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-gray-500">
Showing {startIndex + 1} to{" "}
{Math.min(endIndex, filteredJenisDokIL.length)}{" "}
of {filteredJenisDokIL.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>
<DialogHeader>
<DialogTitle>Konfirmasi Hapus</DialogTitle>
</DialogHeader>
<p>
Apakah anda yakin ingin menghapus jenis dokumen "
{deleteConfirm.NamaJenisDokIL}"?
</p>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteConfirm(null)}
>
Batal
</Button>
<Button
variant="destructive"
onClick={handleDelete}
>
Hapus
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
<Toaster />
</AuthenticatedLayout>
);
}

View File

@ -0,0 +1,467 @@
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 JenisSanksi {
JenisSanksiId: number | null;
NamaJenisSanksi: string;
}
const ITEMS_PER_PAGE = 5;
export default function JenisSanksiIndex({
jenissanksi,
}: PageProps<{ jenissanksi: JenisSanksi[] }>) {
const {
data,
setData,
post,
put,
delete: destroy,
reset,
} = useForm<JenisSanksi>({
JenisSanksiId: null,
NamaJenisSanksi: "",
});
const { toast } = useToast();
const [editing, setEditing] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<JenisSanksi | null>(
null
);
const [search, setSearch] = useState("");
const [filteredJenisSanksi, setFilteredJenisSanksi] = useState(jenissanksi);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
let filtered = jenissanksi;
if (search) {
filtered = filtered.filter((jenissanksi) =>
jenissanksi.NamaJenisSanksi.toLowerCase().includes(
search.toLowerCase()
)
);
}
setFilteredJenisSanksi(filtered);
setCurrentPage(1);
}, [jenissanksi, search]);
const totalPages = Math.ceil(filteredJenisSanksi.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const currentItems = filteredJenisSanksi.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/jenissanksi/${data.JenisSanksiId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Jenis Sanksi berhasil diperbarui",
variant: "default",
});
setIsModalOpen(false);
reset();
setEditing(false);
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat memperbarui Jenis Sanksi",
variant: "destructive",
});
},
});
} else {
post("/admin/jenissanksi", {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Jenis Sanksi berhasil dibuat",
variant: "default",
});
setIsModalOpen(false);
reset();
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat membuat Jenis Sanksi",
variant: "destructive",
});
},
});
}
};
const handleEdit = (jenissanksi: JenisSanksi) => {
setData({ ...jenissanksi });
setEditing(true);
setIsModalOpen(true);
};
const handleDelete = () => {
if (deleteConfirm) {
destroy(`/admin/jenissanksi/${deleteConfirm.JenisSanksiId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Jenis Sanksi berhasil dihapus",
variant: "default",
});
setDeleteConfirm(null);
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat menghapus Jenis Sanksi",
variant: "destructive",
});
setDeleteConfirm(null);
},
});
}
};
return (
<AuthenticatedLayout header={"Jenis Sanksi"}>
<Head title="Jenis Sanksi" />
<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 Jenis Sanksi..."
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 Jenis Sanksi
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
{editing
? "Ubah JenisSanksi"
: "Buat JenisSanksi"}
</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 JenisSanksi"
value={data.NamaJenisSanksi}
onChange={(e) =>
setData(
"NamaJenisSanksi",
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((jenissanksi, index) => (
<TableRow
key={
jenissanksi.JenisSanksiId ||
"new"
}
className="hover:bg-gray-50 transition-colors duration-150"
>
{/* <TableCell>{category.id}</TableCell> */}
<TableCell>
{startIndex + index + 1}
</TableCell>
<TableCell>
{jenissanksi.NamaJenisSanksi}
</TableCell>
<TableCell className="flex gap-2">
<Button
onClick={() =>
handleEdit(jenissanksi)
}
variant="outline"
className="flex items-center gap-2"
>
<Pencil className="h-4 w-4" />
Edit
</Button>
<Button
onClick={() =>
setDeleteConfirm(
jenissanksi
)
}
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, filteredJenisSanksi.length)}{" "}
of {filteredJenisSanksi.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.NamaJenisSanksi}
</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>
);
}

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 Kategori {
KategoriId: number | null;
NamaKategori: string;
}
const ITEMS_PER_PAGE = 5;
export default function KategoriIndex({
kategori,
}: PageProps<{ kategori: Kategori[] }>) {
const {
data,
setData,
post,
put,
delete: destroy,
reset,
} = useForm<Kategori>({
KategoriId: null,
NamaKategori: "",
});
const { toast } = useToast();
const [editing, setEditing] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<Kategori | null>(null);
const [search, setSearch] = useState("");
const [filteredKategori, setFilteredKategori] = useState(kategori);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
let filtered = kategori;
if (search) {
filtered = filtered.filter((kategori) =>
kategori.NamaKategori.toLowerCase().includes(
search.toLowerCase()
)
);
}
setFilteredKategori(filtered);
setCurrentPage(1);
}, [kategori, search]);
const totalPages = Math.ceil(filteredKategori.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const currentItems = filteredKategori.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/kategori/${data.KategoriId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Kategori berhasil diperbarui",
variant: "default",
});
setIsModalOpen(false);
reset();
setEditing(false);
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat memperbarui kategori",
variant: "destructive",
});
},
});
} else {
post("/admin/kategori", {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Kategori berhasil dibuat",
variant: "default",
});
setIsModalOpen(false);
reset();
},
onError: () => {
toast({
title: "Gagal",
description: "Terjadi kesalahan saat membuat kategori",
variant: "destructive",
});
},
});
}
};
const handleEdit = (kategori: Kategori) => {
setData({ ...kategori });
setEditing(true);
setIsModalOpen(true);
};
const handleDelete = () => {
if (deleteConfirm) {
destroy(`/admin/kategori/${deleteConfirm.KategoriId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Kategori berhasil dihapus",
variant: "default",
});
setDeleteConfirm(null);
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat menghapus kategori",
variant: "destructive",
});
setDeleteConfirm(null);
},
});
}
};
return (
<AuthenticatedLayout header={"Kategori Post"}>
<Head title="Kategori Post" />
<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 Kategori..."
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 Kategori
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
{editing
? "Ubah Kategori"
: "Buat Kategori"}
</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 kategori"
value={data.NamaKategori}
onChange={(e) =>
setData(
"NamaKategori",
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((kategori, index) => (
<TableRow
key={kategori.KategoriId || "new"}
className="hover:bg-gray-50 transition-colors duration-150"
>
{/* <TableCell>{category.id}</TableCell> */}
<TableCell>
{startIndex + index + 1}
</TableCell>
<TableCell>
{kategori.NamaKategori}
</TableCell>
<TableCell className="flex gap-2">
<Button
onClick={() =>
handleEdit(kategori)
}
variant="outline"
className="flex items-center gap-2"
>
<Pencil className="h-4 w-4" />
Edit
</Button>
<Button
onClick={() =>
setDeleteConfirm(
kategori
)
}
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, filteredKategori.length)} of{" "}
{filteredKategori.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.NamaKategori}
</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>
);
}

View File

@ -0,0 +1,484 @@
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 HistoryKegiatan {
HistoryKegiatanId: number | null;
NamaHistoryKegiatan: string;
}
const ITEMS_PER_PAGE = 5;
export default function HistoryKegiatanIndex({
historykegiatan,
}: PageProps<{ historykegiatan: HistoryKegiatan[] }>) {
const {
data,
setData,
post,
put,
delete: destroy,
reset,
} = useForm<HistoryKegiatan>({
HistoryKegiatanId: null,
NamaHistoryKegiatan: "",
});
const { toast } = useToast();
const [editing, setEditing] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<HistoryKegiatan | null>(
null
);
const [search, setSearch] = useState("");
const [filteredHistoryKegiatan, setFilteredHistoryKegiatan] =
useState(historykegiatan);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
let filtered = historykegiatan;
if (search) {
filtered = filtered.filter((historykegiatan) =>
historykegiatan.NamaHistoryKegiatan.toLowerCase().includes(
search.toLowerCase()
)
);
}
setFilteredHistoryKegiatan(filtered);
setCurrentPage(1);
}, [historykegiatan, search]);
const totalPages = Math.ceil(
filteredHistoryKegiatan.length / ITEMS_PER_PAGE
);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const currentItems = filteredHistoryKegiatan.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/historykegiatan/${data.HistoryKegiatanId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "History Kegiatan berhasil diperbarui",
variant: "default",
});
setIsModalOpen(false);
reset();
setEditing(false);
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat memperbarui History Kegiatan",
variant: "destructive",
});
},
});
} else {
post("/admin/historykegiatan", {
onSuccess: () => {
toast({
title: "Berhasil",
description: "History Kegiatan berhasil dibuat",
variant: "default",
});
setIsModalOpen(false);
reset();
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat membuat History Kegiatan",
variant: "destructive",
});
},
});
}
};
const handleEdit = (historykegiatan: HistoryKegiatan) => {
setData({ ...historykegiatan });
setEditing(true);
setIsModalOpen(true);
};
const handleDelete = () => {
if (deleteConfirm) {
destroy(
`/admin/historykegiatan/${deleteConfirm.HistoryKegiatanId}`,
{
onSuccess: () => {
toast({
title: "Berhasil",
description: "History Kegiatan berhasil dihapus",
variant: "default",
});
setDeleteConfirm(null);
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat menghapus History Kegiatan",
variant: "destructive",
});
setDeleteConfirm(null);
},
}
);
}
};
return (
<AuthenticatedLayout header={"History Kegiatan"}>
<Head title="History Kegiatan" />
<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 History Kegiatan..."
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 History Kegiatan
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
{editing
? "Ubah History Kegiatan"
: "Buat History Kegiatan"}
</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 History Kegiatan"
value={
data.NamaHistoryKegiatan
}
onChange={(e) =>
setData(
"NamaHistoryKegiatan",
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(
(historykegiatan, index) => (
<TableRow
key={
historykegiatan.HistoryKegiatanId ||
"new"
}
className="hover:bg-gray-50 transition-colors duration-150"
>
{/* <TableCell>{category.id}</TableCell> */}
<TableCell>
{startIndex + index + 1}
</TableCell>
<TableCell>
{
historykegiatan.NamaHistoryKegiatan
}
</TableCell>
<TableCell className="flex gap-2">
<Button
onClick={() =>
handleEdit(
historykegiatan
)
}
variant="outline"
className="flex items-center gap-2"
>
<Pencil className="h-4 w-4" />
Edit
</Button>
<Button
onClick={() =>
setDeleteConfirm(
historykegiatan
)
}
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,
filteredHistoryKegiatan.length
)}{" "}
of {filteredHistoryKegiatan.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.NamaHistoryKegiatan}
</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>
);
}

View File

@ -0,0 +1,479 @@
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 JenisKegiatan {
JenisKegiatanId: number | null;
NamaJenisKegiatan: string;
}
const ITEMS_PER_PAGE = 5;
export default function JenisKegiatanIndex({
jeniskegiatan,
}: PageProps<{ jeniskegiatan: JenisKegiatan[] }>) {
const {
data,
setData,
post,
put,
delete: destroy,
reset,
} = useForm<JenisKegiatan>({
JenisKegiatanId: null,
NamaJenisKegiatan: "",
});
const { toast } = useToast();
const [editing, setEditing] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<JenisKegiatan | null>(
null
);
const [search, setSearch] = useState("");
const [filteredJenisKegiatan, setFilteredJenisKegiatan] =
useState(jeniskegiatan);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
let filtered = jeniskegiatan;
if (search) {
filtered = filtered.filter((jeniskegiatan) =>
jeniskegiatan.NamaJenisKegiatan.toLowerCase().includes(
search.toLowerCase()
)
);
}
setFilteredJenisKegiatan(filtered);
setCurrentPage(1);
}, [jeniskegiatan, search]);
const totalPages = Math.ceil(filteredJenisKegiatan.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const currentItems = filteredJenisKegiatan.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/jeniskegiatan/${data.JenisKegiatanId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Jenis Kegiatan berhasil diperbarui",
variant: "default",
});
setIsModalOpen(false);
reset();
setEditing(false);
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat memperbarui Jenis Kegiatan",
variant: "destructive",
});
},
});
} else {
post("/admin/jeniskegiatan", {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Jenis Kegiatan berhasil dibuat",
variant: "default",
});
setIsModalOpen(false);
reset();
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat membuat Jenis Kegiatan",
variant: "destructive",
});
},
});
}
};
const handleEdit = (jeniskegiatan: JenisKegiatan) => {
setData({ ...jeniskegiatan });
setEditing(true);
setIsModalOpen(true);
};
const handleDelete = () => {
if (deleteConfirm) {
destroy(`/admin/jeniskegiatan/${deleteConfirm.JenisKegiatanId}`, {
onSuccess: () => {
toast({
title: "Berhasil",
description: "Jenis Kegiatan berhasil dihapus",
variant: "default",
});
setDeleteConfirm(null);
},
onError: () => {
toast({
title: "Gagal",
description:
"Terjadi kesalahan saat menghapus Jenis Kegiatan",
variant: "destructive",
});
setDeleteConfirm(null);
},
});
}
};
return (
<AuthenticatedLayout header={"Jenis Kegiatan"}>
<Head title="Jenis Kegiatan" />
<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 Jenis Kegiatan..."
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 Jenis Kegiatan
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
{editing
? "Ubah Jenis Kegiatan"
: "Buat Jenis Kegiatan"}
</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 History Kegiatan"
value={
data.NamaJenisKegiatan
}
onChange={(e) =>
setData(
"NamaJenisKegiatan",
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(
(jeniskegiatan, index) => (
<TableRow
key={
jeniskegiatan.JenisKegiatanId ||
"new"
}
className="hover:bg-gray-50 transition-colors duration-150"
>
{/* <TableCell>{category.id}</TableCell> */}
<TableCell>
{startIndex + index + 1}
</TableCell>
<TableCell>
{
jeniskegiatan.NamaJenisKegiatan
}
</TableCell>
<TableCell className="flex gap-2">
<Button
onClick={() =>
handleEdit(
jeniskegiatan
)
}
variant="outline"
className="flex items-center gap-2"
>
<Pencil className="h-4 w-4" />
Edit
</Button>
<Button
onClick={() =>
setDeleteConfirm(
jeniskegiatan
)
}
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,
filteredJenisKegiatan.length
)}{" "}
of {filteredJenisKegiatan.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.NamaJenisKegiatan}
</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>
);
}

View File

@ -0,0 +1,161 @@
import AuthenticatedLayout from "@/layouts/authenticated-layout";
import { Head } from "@inertiajs/react";
import React from "react";
export default function tentang() {
return (
<AuthenticatedLayout header={"Tentang SKL"}>
<Head title="Tentang SKL" />
<div className="container mx-auto p-6">
<div className="bg-white rounded-xl shadow-lg overflow-hidden">
<div className="flex flex-col md:flex-row">
{/* Left side - Image */}
<div className="md:w-1/3 p-6 bg-gradient-to-br from-green-50 to-blue-50">
<a
href="https://heyzine.com/flip-book/3a966a8c8d.html"
target="_blank"
>
<img
className="rounded-lg shadow-md hover:shadow-xl transition-transform duration-300 w-full h-auto object-cover transform hover:scale-105"
src="/assets/cover-panduan-skl.png"
alt="SKL Cover"
/>
</a>
<div className="mt-6 text-center">
<a
href="#"
className="inline-flex items-center justify-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200"
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
Unduh PDF
</a>
</div>
</div>
{/* Right side - Content */}
<div className="md:w-2/3 p-8">
<h1 className="text-3xl font-bold text-gray-800 mb-6">
STATUS KETAATAN LINGKUNGAN
</h1>
<div className="space-y-6 text-gray-600">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg
className="w-6 h-6 text-green-500 mr-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div>
<p className="text-sm leading-relaxed">
Merupakan bagian dari perangkat lunak
CReASINDO yang dikembangkan oleh PT
MANDIRI CReASINDO.
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg
className="w-6 h-6 text-blue-500 mr-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<p className="text-sm leading-relaxed text-justify">
Kegunaan Produk ini adalah menyediakan
sistem yang dapat memberikan pelayanan
dalam pelaporan, analisa dan evaluasi
pengelolaan lingkungan dilakukan oleh
perusahaan atau kegiatan usaha yang
berada di Propinsi DKI Jakarta.
</p>
</div>
<div className="flex items-start">
<div className="flex-shrink-0">
<svg
className="w-6 h-6 text-yellow-500 mr-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
</div>
<p className="text-sm leading-relaxed text-justify">
Produk ini diberikan kepada BPLHD DKI
JAKARTA melalui Proyek "SISTEM
PENGAWASAN DAN PENGENDALIAN PENCEMARAN
LINGKUNGAN BPLHD DKI JAKARTA 2015"
Perangkat Lunak ini dilindungi oleh
undang-undang negara Republik Indonesia
dan Perjanjian Internasional yang
berkaitan dengan Hak-Hak Kekayaan
Intelektual.
</p>
</div>
</div>
<div className="mt-8 p-4 bg-blue-50 rounded-lg border border-blue-100">
<div className="flex items-center text-blue-600">
<svg
className="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<p className="text-sm font-medium">
Silahkan klik gambar cover di samping
untuk membuka buku panduan versi
flipbook
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}

View File

@ -0,0 +1,487 @@
import AuthenticatedLayout from "@/layouts/authenticated-layout";
import { Head } from "@inertiajs/react";
import React from "react";
import { useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
BadgeCheck,
ChevronLeft,
ChevronRight,
ChevronUp,
Download,
FileText,
Search,
Upload,
} from "lucide-react";
import Select from "react-select";
interface SKLData {
no: number;
namaPerusahaan: string;
tahun: number;
periodePelaporan: string;
skl: number;
spbm: number;
hasil: {
il: string;
al: string;
lb3: string;
sb: string;
bs: string;
stb: string;
lp: string;
kdm: string;
};
stt: string;
se: string;
}
export default function PelaporanIndex() {
const [year, setYear] = useState<string>("2025");
const [quarter, setQuarter] = useState<string>("Triwulan 1");
// const [company, setCompany] = useState<string>("PT Ajinomoto Indonesia");
const companyOptions = [
{ value: "PT Ajinomoto Indonesia", label: "PT Ajinomoto Indonesia" },
{ value: "PT Unilever Indonesia", label: "PT Unilever Indonesia" },
{
value: "PT Indofood Sukses Makmur",
label: "PT Indofood Sukses Makmur",
},
{ value: "PT Mayora Indah", label: "PT Mayora Indah" },
];
const [company, setCompany] = useState<{
value: string;
label: string;
} | null>(companyOptions[0]);
// Color coding helper
const getStatusColor = (status: string) => {
switch (status) {
case "belum":
return "bg-red-100 text-red-800";
case "pending":
return "bg-gray-100 text-gray-800";
case "siap":
return "bg-orange-100 text-orange-800";
case "selesai":
return "bg-green-100 text-green-800";
default:
return "";
}
};
return (
<AuthenticatedLayout header={"Pelaporan SKL"}>
<Head title="Pelaporan SKL" />
<div className="p-8">
{/* Filter Section */}
<div className="mb-6 space-y-4">
<div className="flex justify-between items-center">
<div className="flex gap-4 items-center">
<Select
options={companyOptions}
value={company}
onChange={setCompany}
placeholder="Pilih Perusahaan"
isSearchable
className="w-[300px]"
/>
<Input
type="number"
value={year}
onChange={(e) => setYear(e.target.value)}
className="w-24"
/>
<Select
options={[
{
value: "Triwulan 1",
label: "Triwulan 1",
},
{
value: "Triwulan 2",
label: "Triwulan 2",
},
{
value: "Triwulan 3",
label: "Triwulan 3",
},
{
value: "Triwulan 4",
label: "Triwulan 4",
},
]}
value={{ value: quarter, label: quarter }}
onChange={(newValue) =>
setQuarter(newValue?.value || "Triwulan 1")
}
placeholder="Pilih Periode"
className="w-[150px]"
/>
<Button>
<Search className="w-4 h-4 mr-2" />
Cari
</Button>
</div>
{/* <div className="flex gap-2">
<Button variant="outline">
<Upload className="w-4 h-4 mr-2" />
Import
</Button>
<Button variant="outline">
<Download className="w-4 h-4 mr-2" />
Export
</Button>
<Button variant="outline">
<FileText className="w-4 h-4 mr-2" />
Template
</Button>
</div> */}
</div>
</div>
{/* Table Section */}
<div className="w-full overflow-x-auto">
<Table className="min-w-[1000px] border-collapse">
<TableHeader>
<TableRow className="border-b border-t bg-green-800">
<TableHead
rowSpan={2}
className="w-[50px] border-r border-l text-white"
>
No.
</TableHead>
<TableHead
rowSpan={2}
className="border-r text-center text-white"
>
Nama Perusahaan
</TableHead>
<TableHead
rowSpan={2}
className="border-r text-center text-white"
>
Tahun
</TableHead>
<TableHead
rowSpan={2}
className="border-r text-center text-white"
>
Periode Pelaporan
</TableHead>
<TableHead
rowSpan={2}
className="border-r text-center text-white"
>
SKL (%)
</TableHead>
<TableHead
rowSpan={2}
className="border-r text-center text-white"
>
SPBM (%)
</TableHead>
<TableHead
className="border-r text-center text-white"
colSpan={8}
>
Hasil (Nilai SKL)
</TableHead>
<TableHead
rowSpan={2}
className="border-r text-center text-white"
>
STT
</TableHead>
<TableHead
rowSpan={2}
className="border-r text-center text-white"
>
SE
</TableHead>
</TableRow>
<TableRow className="border-b bg-green-600">
<TableHead className="text-center border-r text-white">
IL
</TableHead>
<TableHead className="text-center border-r text-white">
AL
</TableHead>
<TableHead className="text-center border-r text-white">
LB3
</TableHead>
<TableHead className="text-center border-r text-white">
SB
</TableHead>
<TableHead className="text-center border-r text-white">
BS
</TableHead>
<TableHead className="text-center border-r text-white">
STB
</TableHead>
<TableHead className="text-center border-r text-white">
LP
</TableHead>
<TableHead className="text-center border-r text-white">
KDM
</TableHead>
</TableRow>
</TableHeader>
<TableBody className="border-b">
<TableRow className="border-b">
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l"
>
1
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l"
>
PT Ajinomoto Indonesia
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l"
>
2023
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l"
>
Triwulan 2 / Semester 1
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l "
>
76.70
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l"
>
76.70
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l bg-red-100 text-red-700"
>
00.00
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l bg-red-100 text-red-700"
>
00.00
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l bg-red-100 text-red-700"
>
00.00
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l bg-gray-100 text-gray-700"
>
76.70
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l bg-gray-100 text-gray-700"
>
76.70
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l bg-orange-100 text-orange-700"
>
76.70
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l bg-green-100 text-green-700"
>
76.70
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l bg-green-100 text-green-700"
>
76.70
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l"
>
<FileText className="w-4 h-4" />
</TableCell>
<TableCell
colSpan={1}
className="text-center text-muted-foreground border-r border-l"
>
<FileText className="w-4 h-4" />
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
{/* Pagination */}
{/* <div className="mt-4 flex items-center gap-2 justify-end">
<Button variant="outline" size="icon">
<ChevronLeft className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<ChevronRight className="h-4 w-4" />
</Button>
</div> */}
{/* Legend Section */}
<div className="mt-6 grid grid-cols-2 gap-6">
{/* Keterangan Section */}
<div className="bg-white rounded-lg shadow-sm border p-6">
<div className="flex items-center gap-2 mb-4">
<FileText className="w-5 h-5 text-green-600" />
<h3 className="font-semibold text-lg">
Keterangan
</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">
SKL : Status Ketaatan Lingkungan
</p>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">
SPBM : Status Pemenuhan Baku Mutu
</p>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">
IL : Ijin Lingkungan
</p>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">AL : Air Limbah</p>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">LB3 : Limbah B3</p>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">
SB : Sumber Bergerak
</p>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">
BS : Kebisingan & Udara Ambien
</p>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">
STB : Sumber Tidak Bergerak
</p>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">LP : Limbah Padat</p>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">
KDM : Kawasan Dilarang Merokok
</p>
</div>
</div>
<div className="space-y-2 pt-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">
STT : Surat Tanda Terima
</p>
</div>
</div>
<div className="space-y-2 pt-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<p className="text-sm">
SE : Surat Evaluasi
</p>
</div>
</div>
</div>
</div>
{/* Status Data Section */}
<div className="bg-white rounded-lg shadow-sm border p-6">
<div className="flex items-center gap-2 mb-4">
<BadgeCheck className="w-5 h-5 text-green-600" />
<h3 className="font-semibold text-lg">
Status Data
</h3>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3 p-2 rounded-md bg-red-100">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<p className="text-sm text-red-700">
Data belum diisi
</p>
</div>
<div className="flex items-center gap-3 p-2 rounded-md bg-gray-100">
<div className="w-3 h-3 rounded-full bg-gray-500"></div>
<p className="text-sm text-gray-700">
Data telah diisi dan belum diverifikasi
</p>
</div>
<div className="flex items-center gap-3 p-2 rounded-md bg-orange-100">
<div className="w-3 h-3 rounded-full bg-orange-500"></div>
<p className="text-sm text-orange-700">
Data telah siap untuk diverifikasi
</p>
</div>
<div className="flex items-center gap-3 p-2 rounded-md bg-green-100">
<div className="w-3 h-3 rounded-full bg-green-500"></div>
<p className="text-sm text-green-700">
Data telah diisi dan diverifikasi
</p>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}

Some files were not shown because too many files have changed in this diff Show More