Compare commits
11 Commits
nico/23-au
...
nico/2-sep
| Author | SHA1 | Date | |
|---|---|---|---|
| fa9601e126 | |||
| 7ae83788b4 | |||
| 22ec8d942d | |||
| 9f9a0fb451 | |||
| b6d6583e77 | |||
| a8fd715822 | |||
| f9530c32eb | |||
| f15ef5a275 | |||
| 3a726a3334 | |||
| b21e1f0c2e | |||
| f63249327d |
@@ -1,6 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "cmdpm429r0000vnndkcwslt0h",
|
||||
"name": "warga"
|
||||
}
|
||||
]
|
||||
29
prisma/data/user/roles.json
Normal file
29
prisma/data/user/roles.json
Normal file
@@ -0,0 +1,29 @@
|
||||
[
|
||||
{
|
||||
"id": "role_admin_desa",
|
||||
"name": "ADMIN_DESA",
|
||||
"description": "Administrator Desa",
|
||||
"permissions": ["manage_users", "manage_content", "view_reports"],
|
||||
"isActive": true,
|
||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "role_admin_kesehatan",
|
||||
"name": "ADMIN_KESEHATAN",
|
||||
"description": "Administrator Bidang Kesehatan",
|
||||
"permissions": ["manage_health_data", "view_reports"],
|
||||
"isActive": true,
|
||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "role_admin_sekolah",
|
||||
"name": "ADMIN_SEKOLAH",
|
||||
"description": "Administrator Sekolah",
|
||||
"permissions": ["manage_school_data", "view_reports"],
|
||||
"isActive": true,
|
||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
35
prisma/data/user/users.json
Normal file
35
prisma/data/user/users.json
Normal file
@@ -0,0 +1,35 @@
|
||||
[
|
||||
{
|
||||
"id": "user_admin_desa",
|
||||
"nama": "Admin Desa",
|
||||
"email": "admin.desa@example.com",
|
||||
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q",
|
||||
"roleId": "role_admin_desa",
|
||||
"isActive": true,
|
||||
"lastLogin": "2025-08-31T10:00:00.000Z",
|
||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "user_admin_puskesmas",
|
||||
"nama": "Admin Kesehatan",
|
||||
"email": "admin.kesehatan@example.com",
|
||||
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q",
|
||||
"roleId": "role_admin_kesehatan",
|
||||
"isActive": true,
|
||||
"lastLogin": null,
|
||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"id": "user_admin_sekolah",
|
||||
"nama": "Admin Sekolah",
|
||||
"email": "admin.sekolah@example.com",
|
||||
"password": "$2b$10$XFDWYOJFxQ7ZM5bA0N4Z0O8u0eKYv58wLsaR7h6XK9bqWJ1YQJQ9q",
|
||||
"roleId": "role_admin_sekolah",
|
||||
"isActive": true,
|
||||
"lastLogin": null,
|
||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||
"updatedAt": "2025-09-01T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
@@ -85,7 +85,6 @@ model FileStorage {
|
||||
KontakItem KontakItem[]
|
||||
Pegawai Pegawai[]
|
||||
DesaDigital DesaDigital[]
|
||||
KolaborasiInovasi KolaborasiInovasi[]
|
||||
InfoTekno InfoTekno[]
|
||||
PengaduanMasyarakat PengaduanMasyarakat[]
|
||||
KegiatanDesa KegiatanDesa[]
|
||||
@@ -100,6 +99,8 @@ model FileStorage {
|
||||
DataPerpustakaan DataPerpustakaan[]
|
||||
PegawaiPPID PegawaiPPID[]
|
||||
PerbekelDariMasaKeMasa PerbekelDariMasaKeMasa[]
|
||||
|
||||
MitraKolaborasi MitraKolaborasi[]
|
||||
}
|
||||
|
||||
//========================================= MENU LANDING PAGE ========================================= //
|
||||
@@ -201,7 +202,7 @@ model PrestasiDesa {
|
||||
deskripsi String @db.Text
|
||||
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
|
||||
kategoriId String
|
||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||
imageId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -292,9 +293,9 @@ model PosisiOrganisasiPPID {
|
||||
pegawai PegawaiPPID[]
|
||||
strukturOrganisasi StrukturPPID[] // Relasi balik
|
||||
parentId String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||
}
|
||||
@@ -1638,18 +1639,27 @@ model ProgramKreatif {
|
||||
|
||||
// ========================================= KOLABORASI INOVASI ========================================= //
|
||||
model KolaborasiInovasi {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
tahun Int
|
||||
slug String @db.Text //deskripsi singkat
|
||||
deskripsi String @db.Text //deskripsi panjang
|
||||
slug String @db.Text //deskripsi singkat
|
||||
deskripsi String @db.Text //deskripsi panjang
|
||||
kolaborator String
|
||||
image FileStorage @relation(fields: [imageId], references: [id])
|
||||
imageId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model MitraKolaborasi {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||
imageId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
// ========================================= INFO TEKHNOLOGI TEPAT GUNA ========================================= //
|
||||
@@ -2094,25 +2104,43 @@ model KategoriBuku {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
nama String
|
||||
email String @unique
|
||||
password String
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
email String @unique
|
||||
password String?
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
roleId String
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
instansi String? // Nama instansi (Puskesmas, Sekolah, dll)
|
||||
isActive Boolean @default(true)
|
||||
lastLogin DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
}
|
||||
|
||||
model Role {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
User User[]
|
||||
id String @id @default(cuid())
|
||||
name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH
|
||||
description String?
|
||||
permissions Json // Menyimpan permission dalam format JSON
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
users User[]
|
||||
|
||||
@@map("roles")
|
||||
}
|
||||
|
||||
// Tabel untuk menyimpan permission
|
||||
model Permission {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("permissions")
|
||||
}
|
||||
|
||||
// ========================================= DATA PENDIDIKAN ========================================= //
|
||||
|
||||
@@ -52,8 +52,52 @@ import tempatKegiatan from "./data/pendidikan/pendidikan-non-formal/tempat-kegia
|
||||
import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-program2.json";
|
||||
import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
||||
import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
||||
import roles from "./data/user/roles.json";
|
||||
import users from "./data/user/users.json";
|
||||
|
||||
(async () => {
|
||||
// =========== USER & ROLE ===========
|
||||
for (const r of roles) {
|
||||
await prisma.role.upsert({
|
||||
where: { id: r.id },
|
||||
update: {
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
permissions: r.permissions,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
permissions: r.permissions,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Roles seeded");
|
||||
//users
|
||||
for (const u of users) {
|
||||
await prisma.user.upsert({
|
||||
where: { id: u.id },
|
||||
update: {
|
||||
nama: u.nama,
|
||||
email: u.email,
|
||||
password: u.password,
|
||||
roleId: u.roleId,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
id: u.id,
|
||||
nama: u.nama,
|
||||
email: u.email,
|
||||
password: u.password,
|
||||
roleId: u.roleId,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Users seeded");
|
||||
// =========== LANDING PAGE ===========
|
||||
// =========== SUBMENU PROFILE ===========
|
||||
// =========== PROFILE PEJABAT DESA ===========
|
||||
|
||||
62
src/app/admin/(dashboard)/_com/LeafletMultiMarkerMap.tsx
Normal file
62
src/app/admin/(dashboard)/_com/LeafletMultiMarkerMap.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
|
||||
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
|
||||
import { LatLngExpression } from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import L from 'leaflet';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type MarkerData = {
|
||||
position: [number, number];
|
||||
popup: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
center: [number, number];
|
||||
markers: MarkerData[];
|
||||
zoom?: number;
|
||||
scrollWheelZoom?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export default function LeafletMultiMarkerMap({
|
||||
center,
|
||||
markers,
|
||||
zoom = 13,
|
||||
scrollWheelZoom = true,
|
||||
className = '',
|
||||
style = { height: '100%', width: '100%', zIndex: 0 },
|
||||
}: Props) {
|
||||
// Fix for default marker icons in Next.js
|
||||
useEffect(() => {
|
||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
||||
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
||||
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<MapContainer
|
||||
center={center as LatLngExpression}
|
||||
zoom={zoom}
|
||||
scrollWheelZoom={scrollWheelZoom}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
{markers.map((marker, index) => (
|
||||
<Marker key={index} position={marker.position as LatLngExpression}>
|
||||
<Popup>{marker.popup}</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
</MapContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -55,10 +56,34 @@ const desaDigitalState = proxy({
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.inovasi.desadigital["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
desaDigitalState.findMany.data = res.data?.data ?? [];
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
desaDigitalState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
desaDigitalState.findMany.page = page;
|
||||
desaDigitalState.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.inovasi.desadigital["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
desaDigitalState.findMany.data = res.data.data ?? [];
|
||||
desaDigitalState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
desaDigitalState.findMany.data = [];
|
||||
desaDigitalState.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch desa digital paginated:", err);
|
||||
desaDigitalState.findMany.data = [];
|
||||
desaDigitalState.findMany.totalPages = 1;
|
||||
} finally {
|
||||
desaDigitalState.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -55,10 +56,34 @@ const infoTeknoState = proxy({
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.inovasi.infotekno["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
infoTeknoState.findMany.data = res.data?.data ?? [];
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
infoTeknoState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
infoTeknoState.findMany.page = page;
|
||||
infoTeknoState.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.inovasi.infotekno["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
infoTeknoState.findMany.data = res.data.data ?? [];
|
||||
infoTeknoState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
infoTeknoState.findMany.data = [];
|
||||
infoTeknoState.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch info teknologi paginated:", err);
|
||||
infoTeknoState.findMany.data = [];
|
||||
infoTeknoState.findMany.totalPages = 1;
|
||||
} finally {
|
||||
infoTeknoState.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,12 +6,11 @@ import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const templateForm = z.object({
|
||||
name: z.string().min(1, "Nama minimal 1 karakter"),
|
||||
tahun: z.number().min(4, "Tahun minimal 4 karakter"),
|
||||
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
|
||||
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
||||
kolaborator: z.string().min(1, "Kolaborator minimal 1 karakter"),
|
||||
imageId: z.string().min(1, "Image ID minimal 1 karakter"),
|
||||
name: z.string().min(1, "Nama kolaborasi inovasi harus diisi"),
|
||||
tahun: z.number().min(1900, "Tahun tidak valid").max(new Date().getFullYear() + 1, "Tahun tidak boleh lebih dari tahun depan"),
|
||||
slug: z.string().min(1, "Slug harus dihasilkan otomatis"),
|
||||
deskripsi: z.string().min(1, "Deskripsi harus diisi"),
|
||||
kolaborator: z.string().min(1, "Kolaborator harus diisi"),
|
||||
})
|
||||
|
||||
const defaultForm = {
|
||||
@@ -20,7 +19,6 @@ const defaultForm = {
|
||||
slug: "",
|
||||
deskripsi: "",
|
||||
kolaborator: "",
|
||||
imageId: "",
|
||||
}
|
||||
|
||||
const kolaborasiInovasiState = proxy({
|
||||
@@ -28,27 +26,37 @@ const kolaborasiInovasiState = proxy({
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateForm.safeParse(kolaborasiInovasiState.create.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate form
|
||||
const validation = templateForm.safeParse(kolaborasiInovasiState.create.form);
|
||||
if (!validation.success) {
|
||||
const errorMessages = validation.error.issues
|
||||
.map(issue => `- ${issue.path.join('.')}: ${issue.message}`)
|
||||
.join('\n');
|
||||
return toast.error(`Validasi gagal:\n${errorMessages}`);
|
||||
}
|
||||
|
||||
kolaborasiInovasiState.create.loading = true;
|
||||
|
||||
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["create"].post(
|
||||
kolaborasiInovasiState.create.form
|
||||
);
|
||||
|
||||
if (res.status === 200) {
|
||||
kolaborasiInovasiState.findMany.load();
|
||||
return toast.success("success create");
|
||||
await kolaborasiInovasiState.findMany.load();
|
||||
return { success: true, data: res.data };
|
||||
}
|
||||
console.log(res);
|
||||
return toast.error("failed create");
|
||||
|
||||
console.error('Create failed:', res);
|
||||
toast.error(res.data?.message || "Gagal menyimpan data");
|
||||
return { success: false, error: res.data };
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
console.error('Error in create:', error);
|
||||
toast.error("Terjadi kesalahan saat menyimpan data");
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
} finally {
|
||||
kolaborasiInovasiState.create.loading = false;
|
||||
}
|
||||
@@ -60,13 +68,21 @@ const kolaborasiInovasiState = proxy({
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
load: async (page = 1, limit = 10) => {
|
||||
// Change to arrow function
|
||||
kolaborasiInovasiState.findMany.loading = true; // Use the full path to access the property
|
||||
search: "",
|
||||
year: "",
|
||||
load: async (page = 1, limit = 10, search = "", year?: string) => {
|
||||
kolaborasiInovasiState.findMany.loading = true;
|
||||
kolaborasiInovasiState.findMany.page = page;
|
||||
kolaborasiInovasiState.findMany.search = search;
|
||||
kolaborasiInovasiState.findMany.year = year || "";
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (year) query.year = year;
|
||||
|
||||
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["find-many"].get({
|
||||
query: { page, limit },
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
@@ -124,7 +140,6 @@ const kolaborasiInovasiState = proxy({
|
||||
slug: data.slug,
|
||||
deskripsi: data.deskripsi,
|
||||
kolaborator: data.kolaborator,
|
||||
imageId: data.imageId,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
@@ -179,7 +194,7 @@ const kolaborasiInovasiState = proxy({
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.KolaborasiInovasiGetPayload<{
|
||||
include: { image: true };
|
||||
omit: { isActive: true };
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
|
||||
229
src/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi.ts
Normal file
229
src/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
const mitraKolaborasiForm = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
imageId: z.string().nonempty(),
|
||||
});
|
||||
|
||||
const defaultForm = {
|
||||
name: "",
|
||||
imageId: "",
|
||||
};
|
||||
|
||||
const mitraKolaborasi = proxy({
|
||||
create: {
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = mitraKolaborasiForm.safeParse(mitraKolaborasi.create.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
try {
|
||||
mitraKolaborasi.create.loading = true;
|
||||
const res = await ApiFetch.api.inovasi.mitrakolaborasi["create"].post(
|
||||
mitraKolaborasi.create.form
|
||||
);
|
||||
if (res.status === 200) {
|
||||
mitraKolaborasi.findMany.load();
|
||||
return toast.success("mitraKolaborasi berhasil disimpan!");
|
||||
}
|
||||
return toast.error("Gagal menyimpan mitraKolaborasi");
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
} finally {
|
||||
mitraKolaborasi.create.loading = false;
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
mitraKolaborasi.create.form = { ...defaultForm };
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.MitraKolaborasiGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
mitraKolaborasi.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
mitraKolaborasi.findMany.page = page;
|
||||
mitraKolaborasi.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.inovasi.mitrakolaborasi["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
mitraKolaborasi.findMany.data = res.data.data ?? [];
|
||||
mitraKolaborasi.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
mitraKolaborasi.findMany.data = [];
|
||||
mitraKolaborasi.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch mitraKolaborasi paginated:", err);
|
||||
mitraKolaborasi.findMany.data = [];
|
||||
mitraKolaborasi.findMany.totalPages = 1;
|
||||
} finally {
|
||||
mitraKolaborasi.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.MitraKolaborasiGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
};
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/inovasi/mitrakolaborasi/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
mitraKolaborasi.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch mitraKolaborasi:", res.statusText);
|
||||
mitraKolaborasi.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching mitraKolaborasi:", error);
|
||||
mitraKolaborasi.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
try {
|
||||
mitraKolaborasi.delete.loading = true;
|
||||
const response = await fetch(`/api/inovasi/mitrakolaborasi/del/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
toast.success(result.message || "mitraKolaborasi berhasil dihapus");
|
||||
await mitraKolaborasi.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result.message || "Gagal menghapus mitraKolaborasi");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus mitraKolaborasi");
|
||||
} finally {
|
||||
mitraKolaborasi.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
update: {
|
||||
id: "",
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/api/inovasi/mitrakolaborasi/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
name: data.name,
|
||||
imageId: data.imageId,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading mitraKolaborasi:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async update() {
|
||||
const cek = mitraKolaborasiForm.safeParse(mitraKolaborasi.update.form);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
mitraKolaborasi.update.loading = true;
|
||||
const response = await fetch(`/api/inovasi/mitrakolaborasi/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: this.form.name,
|
||||
imageId: this.form.imageId,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
toast.success(result.message || "mitraKolaborasi berhasil diupdate");
|
||||
await mitraKolaborasi.findMany.load(); // refresh list
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal mengupdate mitraKolaborasi");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating mitraKolaborasi:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal mengupdate mitraKolaborasi"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
mitraKolaborasi.update.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
mitraKolaborasi.update.id = "";
|
||||
mitraKolaborasi.update.form = { ...defaultForm };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default mitraKolaborasi;
|
||||
@@ -56,13 +56,17 @@ const dataLingkunganDesaState = proxy({
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
load: async (page = 1, limit = 10) => {
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
dataLingkunganDesaState.findMany.loading = true; // Use the full path to access the property
|
||||
dataLingkunganDesaState.findMany.page = page;
|
||||
dataLingkunganDesaState.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
const res = await ApiFetch.api.lingkungan.datalingkungandesa["find-many"].get({
|
||||
query: { page, limit },
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -67,10 +68,46 @@ const kegiatanDesa = proxy({
|
||||
};
|
||||
}>
|
||||
> | null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.lingkungan.kegiatandesa["find-many"].get();
|
||||
if (res.status === 200) {
|
||||
kegiatanDesa.findMany.data = res.data?.data ?? [];
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||
// Change to arrow function
|
||||
kegiatanDesa.findMany.loading = true; // Use the full path to access the property
|
||||
kegiatanDesa.findMany.page = page;
|
||||
kegiatanDesa.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
const res = await ApiFetch.api.lingkungan.kegiatandesa[
|
||||
"find-many"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
kegiatanDesa.findMany.data = res.data.data || [];
|
||||
kegiatanDesa.findMany.total = res.data.total || 0;
|
||||
kegiatanDesa.findMany.totalPages = res.data.totalPages || 1;
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to load kegiatan desa:",
|
||||
res.data?.message
|
||||
);
|
||||
kegiatanDesa.findMany.data = [];
|
||||
kegiatanDesa.findMany.total = 0;
|
||||
kegiatanDesa.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading kegiatan desa:", error);
|
||||
kegiatanDesa.findMany.data = [];
|
||||
kegiatanDesa.findMany.total = 0;
|
||||
kegiatanDesa.findMany.totalPages = 1;
|
||||
} finally {
|
||||
kegiatanDesa.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -244,6 +281,35 @@ const kegiatanDesa = proxy({
|
||||
kegiatanDesa.edit.form = { ...defaultKegiatanDesaForm };
|
||||
},
|
||||
},
|
||||
findFirst: {
|
||||
data: null as Prisma.KegiatanDesaGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategoriKegiatan: true;
|
||||
};
|
||||
}> | null,
|
||||
loading: false,
|
||||
// findFirst.load()
|
||||
async load(kategori?: string) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await ApiFetch.api.lingkungan.kegiatandesa["find-first"].get({
|
||||
query: kategori ? { kategori } : {},
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
this.data = res.data.data || null;
|
||||
} else {
|
||||
this.data = null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch kegiatan desa terbaru:", err);
|
||||
this.data = null;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ========================================= KATEGORI kegiatan ========================================= //
|
||||
@@ -269,9 +335,7 @@ const kategoriKegiatan = proxy({
|
||||
}
|
||||
try {
|
||||
kategoriKegiatan.create.loading = true;
|
||||
const res = await ApiFetch.api.lingkungan.kategorikegiatan[
|
||||
"create"
|
||||
].post(kategoriKegiatan.create.form);
|
||||
const res = await ApiFetch.api.lingkungan.kategorikegiatan["create"].post(kategoriKegiatan.create.form);
|
||||
if (res.status === 200) {
|
||||
kategoriKegiatan.findMany.load();
|
||||
return toast.success("Data berhasil ditambahkan");
|
||||
@@ -305,9 +369,7 @@ const kategoriKegiatan = proxy({
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/lingkungan/kategorikegiatan/${id}`
|
||||
);
|
||||
const res = await fetch(`/api/lingkungan/kategorikegiatan/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
kategoriKegiatan.findUnique.data = data.data ?? null;
|
||||
@@ -367,15 +429,12 @@ const kategoriKegiatan = proxy({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/lingkungan/kategorikegiatan/${id}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await fetch(`/api/lingkungan/kategorikegiatan/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -52,15 +52,19 @@ const pengelolaanSampah = proxy({
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
load: async (page = 1, limit = 10) => {
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
pengelolaanSampah.findMany.loading = true; // Use the full path to access the property
|
||||
pengelolaanSampah.findMany.page = page;
|
||||
pengelolaanSampah.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
const res = await ApiFetch.api.lingkungan.pengelolaansampah[
|
||||
"find-many"
|
||||
].get({
|
||||
query: { page, limit },
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
@@ -265,7 +269,7 @@ const keteranganSampah = proxy({
|
||||
try {
|
||||
keteranganSampah.create.loading = true;
|
||||
const res =
|
||||
await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
|
||||
await ApiFetch.api.lingkungan.keteranganbankterdekat[
|
||||
"create"
|
||||
].post(keteranganSampah.create.form);
|
||||
if (res.status === 200) {
|
||||
@@ -287,14 +291,47 @@ const keteranganSampah = proxy({
|
||||
omit: { isActive: true };
|
||||
}>[]
|
||||
| null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
keteranganSampah.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
},
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
keteranganSampah.findMany.loading = true; // Use the full path to access the property
|
||||
keteranganSampah.findMany.page = page;
|
||||
keteranganSampah.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
const res = await ApiFetch.api.lingkungan.keteranganbankterdekat[
|
||||
"find-many"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
keteranganSampah.findMany.data = res.data.data || [];
|
||||
keteranganSampah.findMany.total = res.data.total || 0;
|
||||
keteranganSampah.findMany.totalPages = res.data.totalPages || 1;
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to load keterangan bank sampah terdekat:",
|
||||
res.data?.message
|
||||
);
|
||||
keteranganSampah.findMany.data = [];
|
||||
keteranganSampah.findMany.total = 0;
|
||||
keteranganSampah.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading keterangan bank sampah terdekat:", error);
|
||||
keteranganSampah.findMany.data = [];
|
||||
keteranganSampah.findMany.total = 0;
|
||||
keteranganSampah.findMany.totalPages = 1;
|
||||
} finally {
|
||||
keteranganSampah.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
|
||||
@@ -302,7 +339,7 @@ const keteranganSampah = proxy({
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`);
|
||||
const res = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
keteranganSampah.findUnique.data = data.data ?? null;
|
||||
@@ -324,7 +361,7 @@ const keteranganSampah = proxy({
|
||||
try {
|
||||
keteranganSampah.delete.loading = true;
|
||||
|
||||
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/del/${id}`, {
|
||||
const response = await fetch(`/api/lingkungan/keteranganbankterdekat/del/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -359,7 +396,7 @@ const keteranganSampah = proxy({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`, {
|
||||
const response = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -404,7 +441,7 @@ const keteranganSampah = proxy({
|
||||
try {
|
||||
keteranganSampah.edit.loading = true;
|
||||
const response = await fetch(
|
||||
`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${this.id}`,
|
||||
`/api/lingkungan/keteranganbankterdekat/${this.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
|
||||
@@ -56,13 +56,17 @@ const programPenghijauanState = proxy({
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
load: async (page = 1, limit = 10) => {
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
programPenghijauanState.findMany.loading = true; // Use the full path to access the property
|
||||
programPenghijauanState.findMany.page = page;
|
||||
programPenghijauanState.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
const res = await ApiFetch.api.lingkungan.programpenghijauan["find-many"].get({
|
||||
query: { page, limit },
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
import { proxy } from "valtio";
|
||||
import { z } from "zod";
|
||||
|
||||
// ========================================= BEASISWA PENDAFTAR ========================================= //
|
||||
|
||||
const templateBeasiswaPendaftar = z.object({
|
||||
namaLengkap: z.string().min(1, "Nama harus diisi"),
|
||||
nik: z.string().min(1, "NIK harus diisi"),
|
||||
@@ -76,13 +79,34 @@ const beasiswaPendaftar = proxy({
|
||||
isActive: true;
|
||||
};
|
||||
}>[],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
|
||||
"findMany"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
beasiswaPendaftar.findMany.data = res.data?.data ?? [];
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
beasiswaPendaftar.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
beasiswaPendaftar.findMany.page = page;
|
||||
beasiswaPendaftar.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar["findMany"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
beasiswaPendaftar.findMany.data = res.data.data ?? [];
|
||||
beasiswaPendaftar.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
beasiswaPendaftar.findMany.data = [];
|
||||
beasiswaPendaftar.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch beasiswa pendaftar paginated:", err);
|
||||
beasiswaPendaftar.findMany.data = [];
|
||||
beasiswaPendaftar.findMany.totalPages = 1;
|
||||
} finally {
|
||||
beasiswaPendaftar.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -275,8 +299,260 @@ const beasiswaPendaftar = proxy({
|
||||
},
|
||||
});
|
||||
|
||||
// ========================================= KEUNGGULAN PROGRAM ========================================= //
|
||||
const templateKeunggulanProgram = z.object({
|
||||
judul: z.string().min(1, "Judul harus diisi"),
|
||||
deskripsi: z.string().min(1, "Deskripsi harus diisi"),
|
||||
});
|
||||
|
||||
const defaultKeunggulanProgram = {
|
||||
judul: "",
|
||||
deskripsi: "",
|
||||
};
|
||||
|
||||
const keunggulanProgram = proxy({
|
||||
create: {
|
||||
form: { ...defaultKeunggulanProgram },
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateKeunggulanProgram.safeParse(
|
||||
keunggulanProgram.create.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
return toast.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
keunggulanProgram.create.loading = true;
|
||||
const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram[
|
||||
"create"
|
||||
].post(keunggulanProgram.create.form);
|
||||
if (res.status === 200) {
|
||||
keunggulanProgram.findMany.load();
|
||||
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
|
||||
}
|
||||
console.log(res);
|
||||
return toast.error("failed create");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return toast.error("failed create");
|
||||
} finally {
|
||||
keunggulanProgram.create.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: [] as Prisma.KeunggulanProgramGetPayload<{
|
||||
omit: {
|
||||
isActive: true;
|
||||
};
|
||||
}>[],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
keunggulanProgram.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
keunggulanProgram.findMany.page = page;
|
||||
keunggulanProgram.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram["findMany"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
keunggulanProgram.findMany.data = res.data.data ?? [];
|
||||
keunggulanProgram.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
keunggulanProgram.findMany.data = [];
|
||||
keunggulanProgram.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch keunggulan program paginated:", err);
|
||||
keunggulanProgram.findMany.data = [];
|
||||
keunggulanProgram.findMany.totalPages = 1;
|
||||
} finally {
|
||||
keunggulanProgram.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.KeunggulanProgramGetPayload<{
|
||||
omit: {
|
||||
isActive: true;
|
||||
};
|
||||
}> | null,
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/pendidikan/beasiswa/keunggulanprogram/${id}`
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
keunggulanProgram.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
keunggulanProgram.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
keunggulanProgram.findUnique.data = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
loading: false,
|
||||
async delete(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
|
||||
try {
|
||||
keunggulanProgram.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/pendidikan/beasiswa/keunggulanprogram/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(result.message || "Keunggulan Program berhasil dihapus");
|
||||
await keunggulanProgram.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus keunggulan program");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus keunggulan program");
|
||||
} finally {
|
||||
keunggulanProgram.delete.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
update: {
|
||||
id: "",
|
||||
form: { ...defaultKeunggulanProgram },
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
if (!id) {
|
||||
toast.warn("ID tidak valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/pendidikan/beasiswa/keunggulanprogram/${id}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result?.success) {
|
||||
const data = result.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
judul: data.judul,
|
||||
deskripsi: data.deskripsi,
|
||||
};
|
||||
return data; // Return the loaded data
|
||||
} else {
|
||||
throw new Error(result?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading keunggulan program:", error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal memuat data"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async update() {
|
||||
const cek = templateKeunggulanProgram.safeParse(
|
||||
keunggulanProgram.update.form
|
||||
);
|
||||
if (!cek.success) {
|
||||
const err = `[${cek.error.issues
|
||||
.map((v) => `${v.path.join(".")}`)
|
||||
.join("\n")}] required`;
|
||||
toast.error(err);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
keunggulanProgram.update.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/pendidikan/beasiswa/keunggulanprogram/${this.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
judul: this.form.judul,
|
||||
deskripsi: this.form.deskripsi,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP error! status: ${response.status}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
toast.success("Berhasil update keunggulan program");
|
||||
await keunggulanProgram.findMany.load(); // refresh list
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(result.message || "Gagal update keunggulan program");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating keunggulan program:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Terjadi kesalahan saat update keunggulan program"
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
keunggulanProgram.update.loading = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
keunggulanProgram.update.id = "";
|
||||
keunggulanProgram.update.form = { ...defaultKeunggulanProgram };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const beasiswaDesaState = proxy({
|
||||
beasiswaPendaftar,
|
||||
keunggulanProgram
|
||||
});
|
||||
|
||||
export default beasiswaDesaState;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -51,13 +52,46 @@ const jenjangPendidikan = proxy({
|
||||
id: string;
|
||||
nama: string;
|
||||
}> | null,
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
jenjangPendidikan.findMany.data = res.data?.data ?? [];
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
jenjangPendidikan.findMany.loading = true; // Use the full path to access the property
|
||||
jenjangPendidikan.findMany.page = page;
|
||||
jenjangPendidikan.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.infosekolahpaud.jenjangpendidikan[
|
||||
"find-many"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
jenjangPendidikan.findMany.data = res.data.data || [];
|
||||
jenjangPendidikan.findMany.total = res.data.total || 0;
|
||||
jenjangPendidikan.findMany.totalPages = res.data.totalPages || 1;
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to load jenjang pendidikan:",
|
||||
res.data?.message
|
||||
);
|
||||
jenjangPendidikan.findMany.data = [];
|
||||
jenjangPendidikan.findMany.total = 0;
|
||||
jenjangPendidikan.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading jenjang pendidikan:", error);
|
||||
jenjangPendidikan.findMany.data = [];
|
||||
jenjangPendidikan.findMany.total = 0;
|
||||
jenjangPendidikan.findMany.totalPages = 1;
|
||||
} finally {
|
||||
jenjangPendidikan.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -299,18 +333,64 @@ const lembagaPendidikan = proxy({
|
||||
Prisma.LembagaGetPayload<{
|
||||
include: {
|
||||
jenjangPendidikan: true;
|
||||
siswa: true;
|
||||
pengajar: true;
|
||||
};
|
||||
}>
|
||||
}> & {
|
||||
siswa?: [];
|
||||
pengajar?: [];
|
||||
}
|
||||
> | null,
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
lembagaPendidikan.findMany.data = res.data?.data ?? [];
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||
lembagaPendidikan.findMany.loading = true;
|
||||
lembagaPendidikan.findMany.page = page;
|
||||
lembagaPendidikan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = {
|
||||
page,
|
||||
limit,
|
||||
...(search && { search }),
|
||||
...(jenjangPendidikan && { jenjangPendidikanId: jenjangPendidikan })
|
||||
};
|
||||
|
||||
console.log('Fetching lembaga with query:', query);
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan["find-many"].get({ query });
|
||||
|
||||
console.log('API Response:', res);
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
const data = Array.isArray(res.data.data) ? res.data.data : [];
|
||||
const total = typeof res.data.total === 'number' ? res.data.total : 0;
|
||||
const totalPages = typeof res.data.totalPages === 'number' ? res.data.totalPages : 1;
|
||||
|
||||
lembagaPendidikan.findMany.data = data;
|
||||
lembagaPendidikan.findMany.total = total;
|
||||
lembagaPendidikan.findMany.totalPages = totalPages;
|
||||
|
||||
console.log('Successfully loaded lembaga data:', {
|
||||
count: data.length,
|
||||
total,
|
||||
totalPages
|
||||
});
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to load lembaga pendidikan:",
|
||||
res.data?.message || 'No error message provided'
|
||||
);
|
||||
throw new Error(res.data?.message || 'Failed to load lembaga pendidikan');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading lembaga pendidikan:", error);
|
||||
lembagaPendidikan.findMany.data = [];
|
||||
lembagaPendidikan.findMany.total = 0;
|
||||
lembagaPendidikan.findMany.totalPages = 1;
|
||||
} finally {
|
||||
lembagaPendidikan.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -554,16 +634,55 @@ const siswa = proxy({
|
||||
data: null as Array<
|
||||
Prisma.SiswaGetPayload<{
|
||||
include: {
|
||||
lembaga: true;
|
||||
lembaga: {
|
||||
include: {
|
||||
jenjangPendidikan: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>
|
||||
> | null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
siswa.findMany.data = res.data?.data ?? [];
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
jenjangPendidikan: "",
|
||||
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||
siswa.findMany.loading = true;
|
||||
siswa.findMany.page = page;
|
||||
siswa.findMany.search = search;
|
||||
siswa.findMany.jenjangPendidikan = jenjangPendidikan;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (jenjangPendidikan) query.jenjangPendidikanName = jenjangPendidikan;
|
||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
|
||||
"find-many"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
siswa.findMany.data = res.data.data || [];
|
||||
siswa.findMany.total = res.data.total || 0;
|
||||
siswa.findMany.totalPages = res.data.totalPages || 1;
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to load siswa:",
|
||||
res.data?.message
|
||||
);
|
||||
siswa.findMany.data = [];
|
||||
siswa.findMany.total = 0;
|
||||
siswa.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading siswa:", error);
|
||||
siswa.findMany.data = [];
|
||||
siswa.findMany.total = 0;
|
||||
siswa.findMany.totalPages = 1;
|
||||
} finally {
|
||||
siswa.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -794,16 +913,56 @@ const pengajar = proxy({
|
||||
data: null as Array<
|
||||
Prisma.PengajarGetPayload<{
|
||||
include: {
|
||||
lembaga: true;
|
||||
lembaga: {
|
||||
include: {
|
||||
jenjangPendidikan: true
|
||||
}
|
||||
}
|
||||
};
|
||||
}>
|
||||
> | null,
|
||||
async load() {
|
||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
pengajar.findMany.data = res.data?.data ?? [];
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
jenjangPendidikan: "",
|
||||
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||
// Change to arrow function
|
||||
pengajar.findMany.loading = true; // Use the full path to access the property
|
||||
pengajar.findMany.page = page;
|
||||
pengajar.findMany.search = search;
|
||||
pengajar.findMany.jenjangPendidikan = jenjangPendidikan;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (jenjangPendidikan) query.jenjangPendidikanId = jenjangPendidikan;
|
||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
|
||||
"find-many"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
pengajar.findMany.data = res.data.data || [];
|
||||
pengajar.findMany.total = res.data.total || 0;
|
||||
pengajar.findMany.totalPages = res.data.totalPages || 1;
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to load pengajar:",
|
||||
res.data?.message
|
||||
);
|
||||
pengajar.findMany.data = [];
|
||||
pengajar.findMany.total = 0;
|
||||
pengajar.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading pengajar:", error);
|
||||
pengajar.findMany.data = [];
|
||||
pengajar.findMany.total = 0;
|
||||
pengajar.findMany.totalPages = 1;
|
||||
} finally {
|
||||
pengajar.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -815,7 +974,9 @@ const pengajar = proxy({
|
||||
}> | null,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/pendidikan/infosekolahpaud/pengajar/${id}`);
|
||||
const res = await fetch(
|
||||
`/api/pendidikan/infosekolahpaud/pengajar/${id}`
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
pengajar.findUnique.data = data.data ?? null;
|
||||
@@ -948,7 +1109,8 @@ const pengajar = proxy({
|
||||
result
|
||||
);
|
||||
throw new Error(
|
||||
result?.message || `Gagal mengupdate pengajar (${response.status})`
|
||||
result?.message ||
|
||||
`Gagal mengupdate pengajar (${response.status})`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -54,23 +55,46 @@ const dataPerpustakaan = proxy({
|
||||
},
|
||||
},
|
||||
findMany: {
|
||||
data: [] as Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
kategori: true;
|
||||
image: true;
|
||||
};
|
||||
}>[],
|
||||
loading: false,
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan[
|
||||
"findMany"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
dataPerpustakaan.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
data: null as
|
||||
| Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
kategori: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "", kategori = "") => {
|
||||
dataPerpustakaan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
dataPerpustakaan.findMany.page = page;
|
||||
dataPerpustakaan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (kategori) query.kategori = kategori;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.dataperpustakaan["findMany"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
dataPerpustakaan.findMany.data = res.data.data ?? [];
|
||||
dataPerpustakaan.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
dataPerpustakaan.findMany.data = [];
|
||||
dataPerpustakaan.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data perpustakaan paginated:", err);
|
||||
dataPerpustakaan.findMany.data = [];
|
||||
dataPerpustakaan.findMany.totalPages = 1;
|
||||
} finally {
|
||||
dataPerpustakaan.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as Prisma.DataPerpustakaanGetPayload<{
|
||||
include: {
|
||||
@@ -293,14 +317,34 @@ const kategoriBuku = proxy({
|
||||
isActive: true;
|
||||
};
|
||||
}>[],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku[
|
||||
"findMany"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
kategoriBuku.findMany.data = res.data?.data ?? [];
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
kategoriBuku.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
kategoriBuku.findMany.page = page;
|
||||
kategoriBuku.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.perpustakaandigital.kategoribuku["findMany"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
kategoriBuku.findMany.data = res.data.data ?? [];
|
||||
kategoriBuku.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
kategoriBuku.findMany.data = [];
|
||||
kategoriBuku.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch data kategori buku paginated:", err);
|
||||
kategoriBuku.findMany.data = [];
|
||||
kategoriBuku.findMany.totalPages = 1;
|
||||
} finally {
|
||||
kategoriBuku.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,124 +1,43 @@
|
||||
import { proxy } from 'valtio'
|
||||
import { toast } from 'react-toastify'
|
||||
import ApiFetch from '@/lib/api-fetch'
|
||||
import { Prisma } from '@prisma/client'
|
||||
import { z } from 'zod'
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { proxy } from "valtio";
|
||||
import { toast } from "react-toastify";
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
// 1. Validasi Zod
|
||||
const userSchema = z.object({
|
||||
nama: z.string().min(1, 'Nama harus diisi'),
|
||||
email: z.string().email('Email tidak valid'),
|
||||
password: z.string().min(6, 'Password minimal 6 karakter'),
|
||||
roleId: z.string().optional(),
|
||||
})
|
||||
|
||||
const defaultForm = { nama: '', email: '', password: '', roleId: '' }
|
||||
|
||||
// 2. State Valtio
|
||||
// State Valtio
|
||||
const userState = proxy({
|
||||
// Register
|
||||
register: {
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async submit() {
|
||||
const valid = userSchema.omit({ roleId: true }).safeParse(userState.register.form)
|
||||
if (!valid.success) {
|
||||
const err = valid.error.issues.map(i => i.message).join(', ')
|
||||
return toast.error(err)
|
||||
}
|
||||
try {
|
||||
userState.register.loading = true
|
||||
const res = await ApiFetch.api.user.register.post(userState.register.form)
|
||||
if (res.status === 200) {
|
||||
toast.success('Registrasi berhasil, silakan login')
|
||||
userState.register.form = { ...defaultForm } // reset
|
||||
} else {
|
||||
toast.error(res.data?.message || 'Gagal registrasi')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Terjadi kesalahan saat registrasi')
|
||||
} finally {
|
||||
userState.register.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Login
|
||||
login: {
|
||||
form: { email: '', password: '' },
|
||||
loading: false,
|
||||
async submit() {
|
||||
try {
|
||||
userState.login.loading = true
|
||||
const res = await ApiFetch.api.user.login.post(userState.login.form)
|
||||
if (res.status === 200) {
|
||||
toast.success('Login berhasil')
|
||||
const token = res.data?.data?.token
|
||||
if (typeof token === 'string') {
|
||||
localStorage.setItem('token', token)
|
||||
// Optional: simpan user role untuk otorisasi
|
||||
const user = res.data?.data?.user
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
}
|
||||
} else {
|
||||
toast.error(res.data?.message || 'Login gagal')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Terjadi kesalahan saat login')
|
||||
} finally {
|
||||
userState.login.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// CRUD User (untuk admin)
|
||||
create: {
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async create(isAdmin = false) {
|
||||
const valid = userSchema.safeParse(userState.create.form)
|
||||
if (!valid.success) {
|
||||
const err = valid.error.issues.map(i => i.message).join(', ')
|
||||
return toast.error(err)
|
||||
}
|
||||
try {
|
||||
userState.create.loading = true
|
||||
const endpoint = isAdmin ? 'create' : 'register'
|
||||
const res = await ApiFetch.api.user[endpoint].post(userState.create.form)
|
||||
if (res.status === 200) {
|
||||
toast.success('User berhasil dibuat')
|
||||
userState.findMany.load() // refresh list
|
||||
userState.create.form = { ...defaultForm } // reset form
|
||||
} else {
|
||||
toast.error(res.data?.message || 'Gagal membuat user')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Gagal membuat user')
|
||||
} finally {
|
||||
userState.create.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Find Many
|
||||
findMany: {
|
||||
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
async load() {
|
||||
this.loading = true
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
userState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
userState.findMany.page = page;
|
||||
userState.findMany.search = search;
|
||||
|
||||
try {
|
||||
const res = await ApiFetch.api.user.findMany.get()
|
||||
if (res.status === 200) {
|
||||
this.data = res.data?.data || []
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.user["findMany"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
userState.findMany.data = res.data.data ?? [];
|
||||
userState.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
userState.findMany.data = [];
|
||||
userState.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Gagal muat data user')
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch user paginated:", err);
|
||||
userState.findMany.data = [];
|
||||
userState.findMany.totalPages = 1;
|
||||
} finally {
|
||||
this.loading = false
|
||||
userState.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -128,71 +47,20 @@ const userState = proxy({
|
||||
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
this.loading = true
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`/api/user/findUnique/${id}`)
|
||||
const data = await res.json()
|
||||
const res = await fetch(`/api/user/findUnique/${id}`);
|
||||
const data = await res.json();
|
||||
if (res.status === 200) {
|
||||
this.data = data.data
|
||||
this.data = data.data;
|
||||
} else {
|
||||
toast.error(data.message)
|
||||
toast.error(data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Gagal ambil data user')
|
||||
console.error(e);
|
||||
toast.error("Gagal ambil data user");
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Update
|
||||
update: {
|
||||
id: '',
|
||||
form: { ...defaultForm },
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await fetch(`/api/user/findUnique/${id}`)
|
||||
const data = await res.json()
|
||||
if (res.status === 200) {
|
||||
const user = data.data
|
||||
this.id = user.id
|
||||
this.form = {
|
||||
nama: user.nama,
|
||||
email: user.email,
|
||||
password: '', // jangan kirim password lama
|
||||
roleId: user.roleId,
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Gagal muat data')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async submit() {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await fetch(`/api/user/update/${this.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.form),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (res.status === 200) {
|
||||
toast.success('Update berhasil')
|
||||
userState.findMany.load()
|
||||
} else {
|
||||
toast.error(data.message || 'Gagal update')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Gagal update user')
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -201,35 +69,37 @@ const userState = proxy({
|
||||
delete: {
|
||||
loading: false,
|
||||
async submit(id: string) {
|
||||
this.loading = true
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(`/api/user/del/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
const data = await res.json()
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.status === 200) {
|
||||
toast.success('User dinonaktifkan')
|
||||
userState.findMany.load()
|
||||
toast.success("User dinonaktifkan");
|
||||
userState.findMany.load();
|
||||
} else {
|
||||
toast.error(data.message || 'Gagal hapus')
|
||||
toast.error(data.message || "Gagal hapus");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Gagal hapus user')
|
||||
console.error(e);
|
||||
toast.error("Gagal hapus user");
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const templateRole = z.object({
|
||||
name: z.string().min(1, "Nama harus diisi"),
|
||||
permissions: z.array(z.string()).min(1, "Permission harus diisi"),
|
||||
});
|
||||
|
||||
const defaultRole = {
|
||||
name: "",
|
||||
permissions: [] as string[],
|
||||
};
|
||||
|
||||
const roleState = proxy({
|
||||
@@ -247,10 +117,9 @@ const roleState = proxy({
|
||||
|
||||
try {
|
||||
roleState.create.loading = true;
|
||||
const res =
|
||||
await ApiFetch.api.role[
|
||||
"create"
|
||||
].post(roleState.create.form);
|
||||
const res = await ApiFetch.api.role["create"].post(
|
||||
roleState.create.form
|
||||
);
|
||||
if (res.status === 200) {
|
||||
roleState.findMany.load();
|
||||
return toast.success("Data role Berhasil Dibuat");
|
||||
@@ -273,10 +142,7 @@ const roleState = proxy({
|
||||
}>[],
|
||||
loading: false,
|
||||
async load() {
|
||||
const res =
|
||||
await ApiFetch.api.role[
|
||||
"findMany"
|
||||
].get();
|
||||
const res = await ApiFetch.api.role["findMany"].get();
|
||||
if (res.status === 200) {
|
||||
roleState.findMany.data = res.data?.data ?? [];
|
||||
}
|
||||
@@ -291,9 +157,7 @@ const roleState = proxy({
|
||||
loading: false,
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/role/${id}`
|
||||
);
|
||||
const res = await fetch(`/api/role/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
roleState.findUnique.data = data.data ?? null;
|
||||
@@ -315,22 +179,17 @@ const roleState = proxy({
|
||||
try {
|
||||
roleState.delete.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/role/del/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await fetch(`/api/role/del/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result?.success) {
|
||||
toast.success(
|
||||
result.message || "Data role berhasil dihapus"
|
||||
);
|
||||
toast.success(result.message || "Data role berhasil dihapus");
|
||||
await roleState.findMany.load(); // refresh list
|
||||
} else {
|
||||
toast.error(result?.message || "Gagal menghapus Data role");
|
||||
@@ -354,15 +213,12 @@ const roleState = proxy({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/role/${id}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
const response = await fetch(`/api/role/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
@@ -374,6 +230,7 @@ const roleState = proxy({
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
name: data.name,
|
||||
permissions: data.permissions,
|
||||
};
|
||||
return data; // Return the loaded data
|
||||
} else {
|
||||
@@ -400,18 +257,16 @@ const roleState = proxy({
|
||||
try {
|
||||
roleState.update.loading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/role/${this.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: this.form.name,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const response = await fetch(`/api/role/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: this.form.name,
|
||||
permissions: this.form.permissions,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
@@ -451,6 +306,6 @@ const roleState = proxy({
|
||||
const user = proxy({
|
||||
userState,
|
||||
roleState,
|
||||
})
|
||||
});
|
||||
|
||||
export default user
|
||||
export default user;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Pagination } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -29,19 +29,22 @@ function DesaDigitalSmartVillage() {
|
||||
function ListDesaDigitalSmartVillage({ search }: { search: string }) {
|
||||
const state = useProxy(desaDigitalState)
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = state.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.findMany.load()
|
||||
}, [])
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
|
||||
const filteredData = (state.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.deskripsi.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
const filteredData = data || []
|
||||
|
||||
if (!state.findMany.data) {
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
@@ -68,18 +71,27 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${item.id}`)}>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${item.id}`)}>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)} // ini penting!
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -29,19 +29,22 @@ function InfoTeknologiTepatGuna() {
|
||||
function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
|
||||
const state = useProxy(infoTeknoState)
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = state.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.findMany.load()
|
||||
}, [])
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
|
||||
const filteredData = (state.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.deskripsi.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
const filteredData = data || []
|
||||
|
||||
if (!state.findMany.data) {
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
@@ -68,17 +71,25 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`)}>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`)}>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)} // ini penting!
|
||||
total={totalPages}
|
||||
my="md"
|
||||
/>
|
||||
</Center>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "List Kolaborasi Inovasi",
|
||||
value: "listkolaborasiinovasi",
|
||||
href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"
|
||||
},
|
||||
{
|
||||
label: "Mitra Kolaborasi",
|
||||
value: "mitarakolaborasi",
|
||||
href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi"
|
||||
}
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Kolaborasi Inovasi</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabsKolaborasi;
|
||||
@@ -1,155 +0,0 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import CreateEditor from '../../../_com/createEditor';
|
||||
import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
|
||||
|
||||
function CreateProgramKreatifDesa() {
|
||||
const stateCreate = useProxy(kolaborasiInovasiState)
|
||||
const router = useRouter();
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const resetForm = () => {
|
||||
stateCreate.create.form = {
|
||||
name: "",
|
||||
tahun: 0,
|
||||
slug: "",
|
||||
deskripsi: "",
|
||||
kolaborator: "",
|
||||
imageId: "",
|
||||
}
|
||||
|
||||
setPreviewImage(null);
|
||||
setFile(null);
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!file) {
|
||||
return toast.warn("Pilih file gambar terlebih dahulu");
|
||||
}
|
||||
|
||||
// Upload gambar dulu
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
});
|
||||
|
||||
const uploaded = res.data?.data;
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
}
|
||||
|
||||
// Simpan ID gambar ke form
|
||||
stateCreate.create.form.imageId = uploaded.id;
|
||||
|
||||
// Submit data berita
|
||||
await stateCreate.create.create();
|
||||
|
||||
// Reset form setelah submit
|
||||
resetForm();
|
||||
router.push("/admin/inovasi/kolaborasi-inovasi")
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Create Kolaborasi Inovasi</Title>
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Nama Kolaborasi Inovasi</Text>}
|
||||
placeholder="masukkan nama kolaborasi inovasi"
|
||||
onChange={(val) => stateCreate.create.form.name = val.target.value}
|
||||
/>
|
||||
<TextInput
|
||||
type='number'
|
||||
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
|
||||
placeholder="masukkan tahun"
|
||||
onChange={(val) => stateCreate.create.form.tahun = parseInt(val.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
onChange={(e) => stateCreate.create.form.slug = e.currentTarget.value}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Kolaborasi Inovasi</Text>}
|
||||
placeholder='Masukkan deskripsi singkat kolaborasi inovasi'
|
||||
/>
|
||||
<TextInput
|
||||
onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Kolaborator</Text>}
|
||||
placeholder='Masukkan kolaborator'
|
||||
/>
|
||||
<Box>
|
||||
<Stack gap={"xs"}>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const newImages = files.map((file) => ({
|
||||
file,
|
||||
preview: URL.createObjectURL(file),
|
||||
label: '',
|
||||
}));
|
||||
setFile(newImages[0].file);
|
||||
setPreviewImage(newImages[0].preview); // ← ini yang kurang
|
||||
}}
|
||||
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag images here or click to select files
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Attach as many files as you like, each file should not exceed 5mb
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
</Box>
|
||||
{previewImage ? (
|
||||
<Image alt="" src={previewImage} w={200} h={200} />
|
||||
) : (
|
||||
<Center w={200} h={200} bg={"gray"}>
|
||||
<IconImageInPicture />
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Kolaborasi Inovasi</Text>
|
||||
<CreateEditor
|
||||
value={stateCreate.create.form.deskripsi}
|
||||
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
|
||||
/>
|
||||
</Box>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateProgramKreatifDesa;
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import LayoutTabsKolaborasi from './_lib/layoutTabs';
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<LayoutTabsKolaborasi>
|
||||
{children}
|
||||
</LayoutTabsKolaborasi>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -2,41 +2,34 @@
|
||||
"use client";
|
||||
|
||||
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
||||
import kolaborasiInovasiState from "@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi";
|
||||
import colors from "@/con/colors";
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
FileInput,
|
||||
Image,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react";
|
||||
import { IconArrowBack } from "@tabler/icons-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { useProxy } from "valtio/utils";
|
||||
import kolaborasiInovasiState from "@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi";
|
||||
|
||||
function EditKolaborasiInovasi() {
|
||||
const kolaborasiState = useProxy(kolaborasiInovasiState);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: kolaborasiState.update.form.name || '',
|
||||
deskripsi: kolaborasiState.update.form.deskripsi || '',
|
||||
tahun: kolaborasiState.update.form.tahun || '',
|
||||
slug: kolaborasiState.update.form.slug || '',
|
||||
kolaborator: kolaborasiState.update.form.kolaborator || '',
|
||||
imageId: kolaborasiState.update.form.imageId || ''
|
||||
});
|
||||
|
||||
// Load berita by id saat pertama kali
|
||||
@@ -54,13 +47,7 @@ function EditKolaborasiInovasi() {
|
||||
tahun: data.tahun || '',
|
||||
slug: data.slug || '',
|
||||
kolaborator: data.kolaborator || '',
|
||||
imageId: data.imageId || '',
|
||||
});
|
||||
if (data.image) {
|
||||
if (data?.image?.link) {
|
||||
setPreviewImage(data.image.link);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading berita:", error);
|
||||
@@ -82,22 +69,7 @@ function EditKolaborasiInovasi() {
|
||||
tahun: Number(formData.tahun),
|
||||
slug: formData.slug,
|
||||
kolaborator: formData.kolaborator,
|
||||
imageId: formData.imageId // Keep existing imageId if not changed
|
||||
};
|
||||
|
||||
// Jika ada file baru, upload
|
||||
if (file) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
}
|
||||
|
||||
// Update imageId in global state
|
||||
kolaborasiState.update.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
await kolaborasiState.update.submit();
|
||||
toast.success("Berita berhasil diperbarui!");
|
||||
router.push("/admin/inovasi/kolaborasi-inovasi");
|
||||
@@ -144,28 +116,6 @@ function EditKolaborasiInovasi() {
|
||||
label={<Text fz={"sm"} fw={"bold"}>Kolaborator</Text>}
|
||||
placeholder="masukkan kolaborator"
|
||||
/>
|
||||
|
||||
<FileInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
|
||||
value={file}
|
||||
onChange={async (e) => {
|
||||
if (!e) return;
|
||||
setFile(e);
|
||||
const base64 = await e.arrayBuffer().then((buf) =>
|
||||
"data:image/png;base64," + Buffer.from(buf).toString("base64")
|
||||
);
|
||||
setPreviewImage(base64);
|
||||
}}
|
||||
/>
|
||||
|
||||
{previewImage ? (
|
||||
<Image alt="" src={previewImage} w={200} h={200} />
|
||||
) : (
|
||||
<Center w={200} h={200} bg={"gray"}>
|
||||
<IconImageInPicture />
|
||||
</Center>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Konten</Text>
|
||||
<EditEditor
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
|
||||
import colors from '@/con/colors';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
|
||||
|
||||
function DetailKolaborasiInovasi() {
|
||||
const kolaborasiState = useProxy(kolaborasiInovasiState)
|
||||
@@ -69,10 +69,6 @@ function DetailKolaborasiInovasi() {
|
||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kolaborasiState.findUnique.data?.deskripsi }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={kolaborasiState.findUnique.data?.image?.link} alt="gambar" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Kolaborator</Text>
|
||||
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.kolaborator}</Text>
|
||||
@@ -0,0 +1,113 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { YearPickerInput } from '@mantine/dates';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
|
||||
function CreateProgramKreatifDesa() {
|
||||
const stateCreate = useProxy(kolaborasiInovasiState)
|
||||
const router = useRouter();
|
||||
|
||||
const resetForm = () => {
|
||||
stateCreate.create.form = {
|
||||
name: "",
|
||||
tahun: 0,
|
||||
slug: "",
|
||||
deskripsi: "",
|
||||
kolaborator: "",
|
||||
}
|
||||
}
|
||||
|
||||
// Generate slug from name
|
||||
useEffect(() => {
|
||||
const { name } = stateCreate.create.form;
|
||||
if (name) {
|
||||
const slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-');
|
||||
stateCreate.create.form.slug = slug;
|
||||
}
|
||||
}, [stateCreate.create.form.name]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// Submit data kolaborasi inovasi
|
||||
await stateCreate.create.create();
|
||||
|
||||
// Reset form setelah submit
|
||||
resetForm();
|
||||
router.push("/admin/inovasi/kolaborasi-inovasi");
|
||||
toast.success("Berhasil menambahkan kolaborasi inovasi");
|
||||
} catch (error) {
|
||||
console.error("Error creating kolaborasi inovasi:", error);
|
||||
toast.error("Terjadi kesalahan saat menyimpan data");
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Create Kolaborasi Inovasi</Title>
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Nama Kolaborasi Inovasi</Text>}
|
||||
placeholder="masukkan nama kolaborasi inovasi"
|
||||
onChange={(val) => stateCreate.create.form.name = val.target.value}
|
||||
/>
|
||||
<YearPickerInput
|
||||
clearable
|
||||
value={stateCreate.create.form.tahun ? new Date(stateCreate.create.form.tahun, 0, 1) : null}
|
||||
label="Tahun"
|
||||
placeholder="Pilih tahun"
|
||||
onChange={(dateString: string) => {
|
||||
const year = dateString ? new Date(dateString).getFullYear() : 0;
|
||||
stateCreate.create.form.tahun = year;
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
|
||||
<CreateEditor
|
||||
value={stateCreate.create.form.deskripsi}
|
||||
onChange={(val) => {
|
||||
stateCreate.create.form.deskripsi = val;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<TextInput
|
||||
onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Kolaborator</Text>}
|
||||
placeholder='Masukkan kolaborator'
|
||||
/>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Kolaborasi Inovasi</Text>
|
||||
<CreateEditor
|
||||
value={stateCreate.create.form.deskripsi}
|
||||
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
|
||||
/>
|
||||
</Box>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateProgramKreatifDesa;
|
||||
@@ -3,11 +3,11 @@
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import JudulList from '../../_com/judulList';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import kolaborasiInovasiState from '../../_state/inovasi/kolaborasi-inovasi';
|
||||
import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function KolaborasiInovasi() {
|
||||
@@ -28,22 +28,21 @@ function KolaborasiInovasi() {
|
||||
|
||||
function ListKolaborasiInovasi({ search }: { search: string }) {
|
||||
const listState = useProxy(kolaborasiInovasiState)
|
||||
const { data, loading, page, totalPages, load } = listState.findMany
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10)
|
||||
}, [page])
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
page,
|
||||
totalPages,
|
||||
load,
|
||||
} = listState.findMany
|
||||
|
||||
const filteredData = (data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.deskripsi.toLowerCase().includes(keyword) ||
|
||||
item.slug.toLowerCase().includes(keyword) ||
|
||||
item.kolaborator.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
useEffect(() => {
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
@@ -64,11 +63,11 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '2%', textAlign: 'center' }}>No</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Nama Kolaborasi Inovasi</TableTh>
|
||||
<TableTh style={{ width: '15%', textAlign: 'center' }}>Tahun</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Deskripsi Singkat</TableTh>
|
||||
<TableTh style={{ width: '5%', textAlign: 'center' }}>Detail</TableTh>
|
||||
<TableTh>No</TableTh>
|
||||
<TableTh>Nama Kolaborasi Inovasi</TableTh>
|
||||
<TableTh>Tahun</TableTh>
|
||||
<TableTh>Deskripsi Singkat</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
</Table>
|
||||
@@ -89,21 +88,21 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '1%', textAlign: 'center' }}>No</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Nama Kolaborasi Inovasi</TableTh>
|
||||
<TableTh style={{ width: '5%', textAlign: 'center' }}>Tahun</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Deskripsi Singkat</TableTh>
|
||||
<TableTh style={{ width: '5%', textAlign: 'center' }}>Detail</TableTh>
|
||||
<TableTh>No</TableTh>
|
||||
<TableTh>Nama Kolaborasi Inovasi</TableTh>
|
||||
<TableTh>Tahun</TableTh>
|
||||
<TableTh>Deskripsi Singkat</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '1%', textAlign: 'center' }}>{index + 1}</TableTd>
|
||||
<TableTd style={{ width: '15%' }}>{item.name}</TableTd>
|
||||
<TableTd style={{ width: '5%', textAlign: 'center' }}>{item.tahun}</TableTd>
|
||||
<TableTd style={{ width: '20%' }}>{item.slug}</TableTd>
|
||||
<TableTd style={{ width: '5%', textAlign: 'center' }}>
|
||||
<TableTd>{index + 1}</TableTd>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>{item.tahun}</TableTd>
|
||||
<TableTd>{item.slug}</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/inovasi/kolaborasi-inovasi/${item.id}`)}>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
@@ -116,17 +115,13 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
onChange={(newPage) => load(newPage)} // ini penting!
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
</Box>
|
||||
</Box >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function EditFoto() {
|
||||
const state = useProxy(mitraKolaborasi)
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: state.update.form.name || '',
|
||||
imageId: state.update.form.imageId || ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadFoto = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
try {
|
||||
const data = await state.update.load(id);
|
||||
if (data) {
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
imageId: data.imageId || ''
|
||||
});
|
||||
if (data?.image?.link) {
|
||||
setPreviewImage(data.image.link);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading foto:', error);
|
||||
toast.error('Gagal memuat data foto');
|
||||
}
|
||||
};
|
||||
loadFoto();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
state.update.form = {
|
||||
...state.update.form,
|
||||
name: formData.name,
|
||||
imageId: formData.imageId
|
||||
};
|
||||
if (file) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
});
|
||||
const uploaded = res.data?.data;
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
}
|
||||
state.update.form.imageId = uploaded.id;
|
||||
}
|
||||
await state.update.update();
|
||||
toast.success('Mitra berhasil diperbarui!');
|
||||
router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
|
||||
} catch (error) {
|
||||
console.error('Error updating mitra:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui mitra');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Edit Mitra</Title>
|
||||
<TextInput
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Mitra</Text>}
|
||||
placeholder='Masukkan nama mitra'
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
(formData.name = e.target.value)
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
<Text>Upload Foto</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{ 'image/*': [] }}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag gambar ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format gambar
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewImage ? (
|
||||
<Image alt="" src={previewImage} w={200} h={200} />
|
||||
) : (
|
||||
<Center w={200} h={200} bg={"gray"}>
|
||||
<IconImageInPicture />
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
<Group>
|
||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditFoto;
|
||||
@@ -0,0 +1,136 @@
|
||||
'use client'
|
||||
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
|
||||
function CreateFoto() {
|
||||
const state = useProxy(mitraKolaborasi)
|
||||
const router = useRouter();
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const resetForm = () => {
|
||||
state.create.form = {
|
||||
name: "",
|
||||
imageId: "",
|
||||
};
|
||||
|
||||
setPreviewImage(null)
|
||||
setFile(null)
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!file) {
|
||||
return toast.warn("Pilih file gambar terlebih dahulu");
|
||||
}
|
||||
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
});
|
||||
|
||||
const uploaded = res.data?.data;
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
}
|
||||
|
||||
state.create.form.imageId = uploaded.id;
|
||||
await state.create.create();
|
||||
resetForm();
|
||||
router.push("/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi")
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create Mitra</Title>
|
||||
<TextInput
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Mitra</Text>}
|
||||
placeholder='Masukkan nama mitra'
|
||||
value={state.create.form.name}
|
||||
onChange={(val) => {
|
||||
state.create.form.name = val.target.value;
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{ 'image/*': [] }}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag gambar ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format gambar
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{/* Tampilkan preview kalau ada */}
|
||||
{previewImage && (
|
||||
<Box mt="sm">
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
<Group>
|
||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateFoto;
|
||||
@@ -0,0 +1,187 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
|
||||
function MitraKolaborasi() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Mitra Kolaborasi'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListMitraKolaborasi search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListMitraKolaborasi({ search }: { search: string }) {
|
||||
const listState = useProxy(mitraKolaborasi)
|
||||
const router = useRouter();
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
mitraKolaborasi.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/inovasi/kolaborasi-inovasi")
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
page,
|
||||
totalPages,
|
||||
load,
|
||||
} = listState.findMany
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={650} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper p="md" >
|
||||
<Stack>
|
||||
<JudulList
|
||||
title='List Mitra Kolaborasi'
|
||||
href='/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
|
||||
/>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>No</TableTh>
|
||||
<TableTh>Nama Mitra</TableTh>
|
||||
<TableTh>Image</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
</Table>
|
||||
<Text ta="center">Tidak ada data mitra kolaborasi yang tersedia</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box >
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Mitra Kolaborasi'
|
||||
href='/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
|
||||
/>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>No</TableTh>
|
||||
<TableTh>Nama Mitra</TableTh>
|
||||
<TableTh>Image</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{index + 1}</TableTd>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
<Box style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 4
|
||||
}}>
|
||||
<Image
|
||||
src={item.image?.link || ''}
|
||||
alt={item.name}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (item) {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={mitraKolaborasi.delete.loading || !item}
|
||||
color={"red"}
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (item) {
|
||||
router.push(`/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`);
|
||||
}
|
||||
}}
|
||||
disabled={!item}
|
||||
color={"green"}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)} // ini penting!
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
/>
|
||||
</Center>
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus mitra kolaborasi ini?'
|
||||
/>
|
||||
</Box >
|
||||
);
|
||||
}
|
||||
|
||||
export default MitraKolaborasi;
|
||||
@@ -1,26 +1,27 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
|
||||
import apbdes from "@/app/admin/(dashboard)/_state/landing-page/apbdes";
|
||||
import colors from "@/con/colors";
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import { Dropzone } from "@mantine/dropzone";
|
||||
import { IconArrowBack, IconFile, IconImageInPicture, IconUpload, IconX } from "@tabler/icons-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { useProxy } from "valtio/utils";
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function EditAPBDes() {
|
||||
const apbdesState = useProxy(apbdes);
|
||||
@@ -38,14 +39,14 @@ function EditAPBDes() {
|
||||
fileId: apbdesState.edit.form.fileId || ''
|
||||
});
|
||||
|
||||
// Load sdgs desa by id saat pertama kali
|
||||
// Load APBDes data by id
|
||||
useEffect(() => {
|
||||
const loadKolaborasi = async () => {
|
||||
const loadAPBDes = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await apbdesState.edit.load(id); // akses langsung, bukan dari proxy
|
||||
const data = await apbdesState.edit.load(id);
|
||||
if (data) {
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
@@ -53,202 +54,238 @@ function EditAPBDes() {
|
||||
imageId: data.imageId || '',
|
||||
fileId: data.fileId || ''
|
||||
});
|
||||
if (data.image) {
|
||||
if (data?.image?.link) {
|
||||
setPreviewImage(data.image.link);
|
||||
}
|
||||
}
|
||||
if (data.file) {
|
||||
if (data?.file?.link) {
|
||||
setPreviewDoc(data.file.link);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.image?.link) setPreviewImage(data.image.link);
|
||||
if (data.file?.link) setPreviewDoc(data.file.link);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading apbdes:", error);
|
||||
toast.error("Gagal memuat data apbdes");
|
||||
console.error('Error loading APBDes:', error);
|
||||
toast.error('Gagal memuat data APBDes');
|
||||
}
|
||||
};
|
||||
|
||||
loadKolaborasi();
|
||||
loadAPBDes();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
|
||||
try {
|
||||
// edit global state with form data
|
||||
// Update global state with form data
|
||||
apbdesState.edit.form = {
|
||||
...apbdesState.edit.form,
|
||||
name: formData.name,
|
||||
jumlah: formData.jumlah,
|
||||
imageId: formData.imageId // Keep existing imageId if not changed
|
||||
...formData,
|
||||
};
|
||||
|
||||
// Jika ada file image baru, upload
|
||||
// Upload new image if exists
|
||||
if (imageFile) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name });
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file: imageFile,
|
||||
name: imageFile.name
|
||||
});
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
return toast.error('Gagal upload gambar');
|
||||
}
|
||||
|
||||
// edit imageId in global state
|
||||
apbdesState.edit.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
// Jika ada file doc baru, upload
|
||||
// Upload new document if exists
|
||||
if (docFile) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name });
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file: docFile,
|
||||
name: docFile.name
|
||||
});
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload doc");
|
||||
return toast.error('Gagal upload dokumen');
|
||||
}
|
||||
|
||||
// edit fileId in global state
|
||||
apbdesState.edit.form.fileId = uploaded.id;
|
||||
}
|
||||
|
||||
await apbdesState.edit.update();
|
||||
toast.success("apbdes berhasil diperbarui!");
|
||||
router.push("/admin/landing-page/APBDes");
|
||||
toast.success('APBDes berhasil diperbarui!');
|
||||
router.push('/admin/landing-page/APBDes');
|
||||
} catch (error) {
|
||||
console.error("Error updating apbdes:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui apbdes");
|
||||
console.error('Error updating APBDes:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui APBDes');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Edit APBDes</Title>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit APBDes
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama APBDes"
|
||||
placeholder="Masukkan nama APBDes"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
|
||||
placeholder="masukkan nama"
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Jumlah Anggaran"
|
||||
placeholder="Masukkan jumlah anggaran"
|
||||
value={formData.jumlah}
|
||||
onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Jumlah</Text>}
|
||||
placeholder="masukkan jumlah"
|
||||
required
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setImageFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih image
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Image</Text>
|
||||
{previewImage ? (
|
||||
<iframe
|
||||
src={previewImage}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Doc</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setDocFile(selectedFile);
|
||||
setPreviewDoc(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.pdf', '.doc', '.docx'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar APBDes
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setImageFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Stack gap="xs" align="center">
|
||||
<Text size="md" fw={500}>
|
||||
Seret gambar atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Maksimal 5MB, format gambar wajib
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format doc
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewDoc ? (
|
||||
<iframe
|
||||
src={previewDoc}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada doc tersedia</Text>
|
||||
)}
|
||||
{previewImage && (
|
||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
objectFit: 'contain',
|
||||
border: `1px solid ${colors['blue-button']}`
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Button onClick={handleSubmit}>Simpan</Button>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Dokumen APBDes
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setDocFile(selectedFile);
|
||||
setPreviewDoc(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
|
||||
maxSize={10 * 1024 ** 2} // 10MB
|
||||
accept={{
|
||||
'application/pdf': ['.pdf'],
|
||||
'application/msword': ['.doc'],
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
|
||||
'application/vnd.ms-excel': ['.xls'],
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
|
||||
}}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={150}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconFile size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Stack gap="xs" align="center">
|
||||
<Text size="md" fw={500}>
|
||||
Seret dokumen atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Maksimal 10MB, format PDF/DOC/DOCX/XLS/XLSX
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewDoc && (
|
||||
<Box mt="sm">
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
Dokumen terpilih: {docFile?.name || 'Dokumen'}
|
||||
</Text>
|
||||
<Button
|
||||
component="a"
|
||||
href={previewDoc}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="light"
|
||||
leftSection={<IconFile size={16} />}
|
||||
size="sm"
|
||||
>
|
||||
Lihat Dokumen
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { ActionIcon, Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconFile, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconEdit, IconFile, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -35,89 +35,125 @@ function DetailAPBDes() {
|
||||
if (!apbdesState.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={40} />
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = apbdesState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail APBDes</Text>
|
||||
{apbdesState.findUnique.data ? (
|
||||
<Paper key={apbdesState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Nama APBDes</Text>
|
||||
<Text fz={"lg"}>{apbdesState.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Jumlah</Text>
|
||||
<Text fz={"lg"}>{apbdesState.findUnique.data?.jumlah}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={apbdesState.findUnique.data?.image?.link} alt="gambar" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{apbdesState.findUnique.data?.file?.link ? (
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={apbdesState.findUnique.data.file.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="transparent"
|
||||
>
|
||||
<IconFile size={25} color={colors['blue-button']}/>
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Box py={10}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail APBDes
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Nama APBDes</Text>
|
||||
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Jumlah Anggaran</Text>
|
||||
<Text fz="md" c="dimmed">Rp. {data.jumlah || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Gambar</Text>
|
||||
{data.image?.link ? (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.name || 'Gambar APBDes'}
|
||||
w={200}
|
||||
height={150}
|
||||
radius="md"
|
||||
fit="contain"
|
||||
style={{ border: '1px solid #ddd' }}
|
||||
/>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Dokumen</Text>
|
||||
{data.file?.link ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (apbdesState.findUnique.data) {
|
||||
setSelectedId(apbdesState.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={apbdesState.delete.loading || !apbdesState.findUnique.data}
|
||||
color={"red"}
|
||||
component="a"
|
||||
href={data.file.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="light"
|
||||
leftSection={<IconFile size={18} />}
|
||||
size="sm"
|
||||
mt="xs"
|
||||
>
|
||||
<IconX size={20} />
|
||||
Lihat Dokumen
|
||||
</Button>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada dokumen</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group gap="sm" mt="md">
|
||||
<Tooltip label="Hapus APBDes" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
if (apbdesState.findUnique.data) {
|
||||
router.push(`/admin/landing-page/APBDes/${apbdesState.findUnique.data.id}/edit`);
|
||||
}
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
disabled={!apbdesState.findUnique.data}
|
||||
color={"green"}
|
||||
disabled={apbdesState.delete.loading}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Edit APBDes" withArrow position="top">
|
||||
<Button
|
||||
color="green"
|
||||
onClick={() => router.push(`/admin/landing-page/APBDes/${data.id}/edit`)}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus APBDes ini?'
|
||||
text="Apakah Anda yakin ingin menghapus APBDes ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconFile, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -70,140 +81,168 @@ function CreateAPBDes() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah APBDes
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create APBDes</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Gambar APBDes */}
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setImageFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar APBDes
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setImageFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Box>
|
||||
<Text size="xl" inline>
|
||||
Seret gambar atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline display="block" mt={7}>
|
||||
Maksimal 5MB (format: JPEG, JPG, PNG, GIF, WEBP, SVG)
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewImage ? (
|
||||
<iframe
|
||||
src={previewImage}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
{previewImage && (
|
||||
<Box mt="md" style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Dokumen APBDes */}
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Dokumen</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setDocFile(selectedFile);
|
||||
setPreviewDoc(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.pdf', '.doc', '.docx'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Dokumen APBDes
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setDocFile(selectedFile);
|
||||
setPreviewDoc(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format PDF, DOC, atau DOCX')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{
|
||||
'application/pdf': ['.pdf'],
|
||||
'application/msword': ['.doc'],
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
|
||||
}}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconFile size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Box>
|
||||
<Text size="xl" inline>
|
||||
Seret dokumen atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline display="block" mt={7}>
|
||||
Maksimal 5MB (format: PDF, DOC, DOCX)
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format dokumen
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewDoc ? (
|
||||
<iframe
|
||||
src={previewDoc}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
{previewDoc && (
|
||||
<Box mt="md">
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Pratinjau Dokumen
|
||||
</Text>
|
||||
<iframe
|
||||
src={previewDoc}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: '1px solid #ddd', borderRadius: '8px' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Form Input */}
|
||||
<TextInput
|
||||
value={stateAPBDes.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateAPBDes.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
label="Nama APBDes"
|
||||
placeholder="Masukkan nama APBDes"
|
||||
value={stateAPBDes.create.form.name || ''}
|
||||
onChange={(e) => (stateAPBDes.create.form.name = e.target.value)}
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
value={stateAPBDes.create.form.jumlah}
|
||||
onChange={(val) => {
|
||||
stateAPBDes.create.form.jumlah = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Jumlah</Text>}
|
||||
placeholder='Masukkan jumlah'
|
||||
<TextInput
|
||||
label="Jumlah Anggaran"
|
||||
placeholder="Masukkan jumlah anggaran"
|
||||
value={stateAPBDes.create.form.jumlah || ''}
|
||||
onChange={(e) => (stateAPBDes.create.form.jumlah = e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
disabled={!imageFile || !docFile || !stateAPBDes.create.form.name || !stateAPBDes.create.form.jumlah}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { ActionIcon, Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconFile, IconSearch } from '@tabler/icons-react';
|
||||
import { IconDeviceImacCog, IconFile, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import JudulList from '../../_com/judulList';
|
||||
import apbdes from '../../_state/landing-page/apbdes';
|
||||
|
||||
|
||||
@@ -17,7 +16,7 @@ function APBDes() {
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='APBDes'
|
||||
placeholder='pencarian'
|
||||
placeholder='Cari APBDes...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
@@ -48,73 +47,102 @@ function ListAPBDes({ search }: { search: string }) {
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<JudulList
|
||||
title='List APBDes'
|
||||
href='/admin/landing-page/APBDes/create'
|
||||
/>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama APBDes</TableTh>
|
||||
<TableTh>Jumlah APBDes</TableTh>
|
||||
<TableTh>Document</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar APBDes</Title>
|
||||
<Tooltip label="Tambah APBDes" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/APBDes/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '30%' }}>Nama APBDes</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Jumlah</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Dokumen</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||
</Box>
|
||||
<Text fw={500} truncate="end">{item.name}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>Rp. {item.jumlah}</Text>
|
||||
<Text>Rp. {item.jumlah}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
{item.file?.link ? (
|
||||
<ActionIcon
|
||||
<Button
|
||||
component="a"
|
||||
href={item.file.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant='transparent'
|
||||
variant="light"
|
||||
leftSection={<IconFile size={18} />}
|
||||
size="sm"
|
||||
>
|
||||
<IconFile size={25} color={colors['blue-button']} />
|
||||
</ActionIcon>
|
||||
Lihat Dokumen
|
||||
</Button>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
<Text c="dimmed" fz="sm">Tidak ada dokumen</Text>
|
||||
)}
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/APBDes/${item.id}`)}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/landing-page/APBDes/${item.id}`)}
|
||||
fullWidth
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada data APBDes yang cocok</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Center>
|
||||
|
||||
<Center mt="md">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
my={"md"}
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
|
||||
@@ -1,60 +1,103 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { IconList, IconCategory } from '@tabler/icons-react';
|
||||
import colors from '@/con/colors';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "List Desa Anti Korupsi",
|
||||
value: "listDesaAntiKorupsi",
|
||||
href: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi"
|
||||
href: "/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi",
|
||||
icon: <IconList size={18} stroke={1.8} />,
|
||||
tooltip: "Kelola daftar program desa anti korupsi",
|
||||
},
|
||||
{
|
||||
label: "Kategori Desa Anti Korupsi",
|
||||
value: "kategoriDesaAntiKorupsi",
|
||||
href: "/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi"
|
||||
href: "/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi",
|
||||
icon: <IconCategory size={18} stroke={1.8} />,
|
||||
tooltip: "Kelola kategori desa anti korupsi",
|
||||
},
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
|
||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
const tab = tabs.find(t => t.value === value);
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
router.push(tab.href);
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
const match = tabs.find(tab => tab.href === pathname);
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
setActiveTab(match.value);
|
||||
}
|
||||
}, [pathname])
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Desa Anti Korupsi</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
<Stack gap="lg">
|
||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||
Desa Anti Korupsi
|
||||
</Title>
|
||||
<Tabs
|
||||
color={colors['blue-button']}
|
||||
variant="pills"
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
key={i}
|
||||
value={tab.value}
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
'use client'
|
||||
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function EditKategoriDesaAntiKorupsi() {
|
||||
export default function EditKategoriDesaAntiKorupsi() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params?.id as string;
|
||||
@@ -18,16 +18,17 @@ function EditKategoriDesaAntiKorupsi() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadKategorikegiatan = async () => {
|
||||
const loadKategori = async () => {
|
||||
if (!id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const data = await stateKategori.edit.load(id);
|
||||
|
||||
if (data) {
|
||||
// pastikan id-nya masuk ke state edit
|
||||
stateKategori.edit.id = id;
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
@@ -36,63 +37,88 @@ function EditKategoriDesaAntiKorupsi() {
|
||||
} catch (error) {
|
||||
console.error("Error loading kategori desa anti korupsi:", error);
|
||||
toast.error("Gagal memuat data kategori desa anti korupsi");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadKategorikegiatan();
|
||||
loadKategori();
|
||||
}, [id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (!formData.name.trim()) {
|
||||
toast.error('Nama kategori desa anti korupsi tidak boleh kosong');
|
||||
return;
|
||||
}
|
||||
if (!formData.name.trim()) {
|
||||
return toast.error('Nama kategori tidak boleh kosong');
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
stateKategori.edit.form = {
|
||||
name: formData.name.trim(),
|
||||
};
|
||||
|
||||
// Safety check tambahan: pastikan ID tidak kosong
|
||||
if (!stateKategori.edit.id) {
|
||||
stateKategori.edit.id = id; // fallback
|
||||
stateKategori.edit.id = id;
|
||||
}
|
||||
|
||||
const success = await stateKategori.edit.update();
|
||||
|
||||
if (success) {
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
|
||||
}
|
||||
await stateKategori.edit.update();
|
||||
toast.success('Kategori berhasil diperbarui');
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
|
||||
} catch (error) {
|
||||
console.error("Error updating kategori desa anti korupsi:", error);
|
||||
// toast akan ditampilkan dari fungsi update
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui kategori');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Kategori Desa Anti Korupsi
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Edit Kategori Desa Anti Korupsi</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Kategori"
|
||||
placeholder="Masukkan nama kategori"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>}
|
||||
placeholder='Masukkan nama kategori desa anti korupsi'
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
loading={isLoading}
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditKategoriDesaAntiKorupsi;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import korupsiState from '../../../../_state/landing-page/desa-anti-korupsi';
|
||||
|
||||
function CreateKategoriDesaAntiKorupsi() {
|
||||
export default function CreateKategoriDesaAntiKorupsi() {
|
||||
const router = useRouter();
|
||||
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
|
||||
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
|
||||
|
||||
useEffect(() => {
|
||||
stateKategori.findMany.load();
|
||||
@@ -20,42 +20,64 @@ function CreateKategoriDesaAntiKorupsi() {
|
||||
stateKategori.create.form = {
|
||||
name: "",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!stateKategori.create.form.name) {
|
||||
return alert('Nama kategori harus diisi');
|
||||
}
|
||||
|
||||
await stateKategori.create.create();
|
||||
resetForm();
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi")
|
||||
}
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Kategori Desa Anti Korupsi
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create Kategori Desa Anti Korupsi</Title>
|
||||
<TextInput
|
||||
value={stateKategori.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateKategori.create.form.name = val.target.value;
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Kategori"
|
||||
placeholder="Masukkan nama kategori"
|
||||
value={stateKategori.create.form.name || ''}
|
||||
onChange={(e) => (stateKategori.create.form.name = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Desa Anti Korupsi</Text>}
|
||||
placeholder='Masukkan nama kategori desa anti korupsi'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateKategoriDesaAntiKorupsi;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
|
||||
|
||||
@@ -56,74 +55,84 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
// Handle loading state
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={550} />
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Kategori Kegiatan'
|
||||
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
|
||||
/>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Kategori Kegiatan'
|
||||
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
|
||||
/>
|
||||
<Box style={{ overflowY: "auto" }}>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Kategori Kegiatan</Title>
|
||||
<Tooltip label="Tambah Kategori" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Kategori</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
<TableTh>Hapus</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
<Button color="green" onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button color="red" onClick={() => {
|
||||
setSelectedId(item.id)
|
||||
setModalHapus(true)
|
||||
}}>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fw={500}>{item.name}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Tooltip label="Edit" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Tooltip label="Hapus" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={18} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={2}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed">Tidak ada data kategori yang ditemukan</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
@@ -133,11 +142,13 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo(0, 0);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
|
||||
'use client';
|
||||
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
|
||||
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { toast } from 'react-toastify';
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
|
||||
interface FormDesaAntiKorupsi {
|
||||
@@ -22,18 +20,20 @@ interface FormDesaAntiKorupsi {
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
function EditDesaAntiKorupsi() {
|
||||
const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi)
|
||||
export default function EditDesaAntiKorupsi() {
|
||||
const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi);
|
||||
const [previewFile, setPreviewFile] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const [formData, setFormData] = useState<FormDesaAntiKorupsi>({
|
||||
name: '',
|
||||
deskripsi: '',
|
||||
kategoriId: '',
|
||||
fileId: '',
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadDesaAntiKorupsi = async () => {
|
||||
@@ -43,7 +43,6 @@ function EditDesaAntiKorupsi() {
|
||||
try {
|
||||
const data = await desaAntiKorupsiState.edit.load(id);
|
||||
if (data) {
|
||||
// ⬇️ FIX PENTING: tambahkan ini
|
||||
desaAntiKorupsiState.edit.id = id;
|
||||
|
||||
desaAntiKorupsiState.edit.form = {
|
||||
@@ -61,169 +60,198 @@ function EditDesaAntiKorupsi() {
|
||||
});
|
||||
|
||||
if (data?.file?.link) {
|
||||
setPreviewFile(data.file.link)
|
||||
setPreviewFile(data.file.link);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading program penghijauan:", error);
|
||||
toast.error("Gagal memuat data program penghijauan");
|
||||
console.error('Error loading data:', error);
|
||||
toast.error('Gagal memuat data Desa Anti Korupsi');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadDesaAntiKorupsi();
|
||||
}, [params?.id]);
|
||||
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.name) {
|
||||
return toast.warn('Masukkan judul dokumen');
|
||||
}
|
||||
if (!formData.kategoriId) {
|
||||
return toast.warn('Pilih kategori dokumen');
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Update global state with form data
|
||||
desaAntiKorupsiState.edit.form = {
|
||||
...desaAntiKorupsiState.edit.form,
|
||||
name: formData.name,
|
||||
deskripsi: formData.deskripsi,
|
||||
...formData,
|
||||
kategoriId: formData.kategoriId || '',
|
||||
fileId: formData.fileId // Keep existing imageId if not changed
|
||||
};
|
||||
|
||||
// Jika ada file baru, upload
|
||||
// Upload new file if exists
|
||||
if (file) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name
|
||||
});
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
throw new Error('Gagal mengunggah dokumen');
|
||||
}
|
||||
|
||||
// Update imageId in global state
|
||||
desaAntiKorupsiState.edit.form.fileId = uploaded.id;
|
||||
}
|
||||
|
||||
await desaAntiKorupsiState.edit.update();
|
||||
toast.success("desa anti korupsi berhasil diperbarui!");
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi");
|
||||
toast.success('Data berhasil diperbarui');
|
||||
router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
|
||||
} catch (error) {
|
||||
console.error("Error updating desa anti korupsi:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui desa anti korupsi");
|
||||
console.error('Error updating data:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Edit List Desa Anti Korupsi</Text>
|
||||
{desaAntiKorupsiState.findUnique.data ? (
|
||||
<Paper key={desaAntiKorupsiState.findUnique.data.id}>
|
||||
<Stack gap={"xs"}>
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
name: val.target.value
|
||||
})
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Desa Anti Korupsi
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Judul Dokumen"
|
||||
placeholder="Masukkan judul dokumen"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Select
|
||||
label="Kategori"
|
||||
placeholder="Pilih kategori"
|
||||
value={formData.kategoriId}
|
||||
onChange={(val) => setFormData({ ...formData, kategoriId: val || '' })}
|
||||
data={
|
||||
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
required
|
||||
searchable
|
||||
clearable
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Dokumen
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{
|
||||
'application/*': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'],
|
||||
}}
|
||||
radius="md"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconFile size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<div>
|
||||
<Text size="lg" inline>
|
||||
Seret dokumen ke sini atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewFile && (
|
||||
<Box mt="md">
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Pratinjau Dokumen
|
||||
</Text>
|
||||
<Box
|
||||
style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
deskripsi: val
|
||||
})
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
src={previewFile}
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 'none' }}
|
||||
/>
|
||||
</Box>
|
||||
<Select
|
||||
value={formData.kategoriId}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
kategoriId: val ?? ""
|
||||
})
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||
placeholder="Pilih kategori"
|
||||
data={
|
||||
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Document</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.pdf', '.doc', '.docx'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format document
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewFile ? (
|
||||
<iframe
|
||||
src={previewFile}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
<Group justify="right" mt="xl">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
loading={isLoading}
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditDesaAntiKorupsi;
|
||||
}
|
||||
@@ -2,15 +2,15 @@
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function DetailKegiatanDesa() {
|
||||
export default function DetailKegiatanDesa() {
|
||||
const detailState = useProxy(korupsiState.desaAntikorupsi)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
@@ -34,89 +34,123 @@ function DetailKegiatanDesa() {
|
||||
if (!detailState.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={40} />
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = detailState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail List Desa Anti Korupsi</Text>
|
||||
{detailState.findUnique.data ? (
|
||||
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
||||
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
|
||||
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{detailState.findUnique.data?.file?.link ? (
|
||||
<iframe
|
||||
src={detailState.findUnique.data.file.link}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (detailState.findUnique.data) {
|
||||
setSelectedId(detailState.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
<Box py={10}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Desa Anti Korupsi
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Judul</Text>
|
||||
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Kategori</Text>
|
||||
<Text fz="md" c="dimmed">{data.kategori?.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold" mb="xs">Deskripsi</Text>
|
||||
<Box
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
style={{ lineHeight: 1.6 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold" mb="xs">Dokumen</Text>
|
||||
{data.file?.link ? (
|
||||
<Box
|
||||
style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
height: '500px',
|
||||
width: '100%'
|
||||
}}
|
||||
disabled={detailState.delete.loading || !detailState.findUnique.data}
|
||||
color={"red"}
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
<iframe
|
||||
src={data.file.link}
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 'none' }}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group gap="sm" mt="md">
|
||||
<Tooltip label="Hapus Data" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
if (detailState.findUnique.data) {
|
||||
router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${detailState.findUnique.data.id}/edit`);
|
||||
}
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
disabled={!detailState.findUnique.data}
|
||||
color={"green"}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={detailState.delete.loading}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Edit Data" withArrow position="top">
|
||||
<Button
|
||||
color="green"
|
||||
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${data.id}/edit`)}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus desa anti korupsi ini?'
|
||||
text="Apakah Anda yakin ingin menghapus data Desa Anti Korupsi ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailKegiatanDesa;
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -13,12 +24,12 @@ import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function CreateDesaAntiKorupsi() {
|
||||
export default function CreateDesaAntiKorupsi() {
|
||||
const router = useRouter();
|
||||
const stateKorupsi = useProxy(korupsiState.desaAntikorupsi)
|
||||
const stateKorupsi = useProxy(korupsiState.desaAntikorupsi);
|
||||
const [previewFile, setPreviewFile] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
stateKorupsi.findMany.load();
|
||||
@@ -27,140 +38,181 @@ function CreateDesaAntiKorupsi() {
|
||||
|
||||
const resetForm = () => {
|
||||
stateKorupsi.create.form = {
|
||||
name: "",
|
||||
deskripsi: "",
|
||||
kategoriId: "",
|
||||
fileId: "",
|
||||
name: '',
|
||||
deskripsi: '',
|
||||
kategoriId: '',
|
||||
fileId: '',
|
||||
};
|
||||
setFile(null);
|
||||
setPreviewFile(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!file) {
|
||||
return toast.warn("Pilih file pdf terlebih dahulu");
|
||||
return toast.warn('Pilih file dokumen terlebih dahulu');
|
||||
}
|
||||
if (!stateKorupsi.create.form.name) {
|
||||
return toast.warn('Masukkan judul dokumen');
|
||||
}
|
||||
if (!stateKorupsi.create.form.kategoriId) {
|
||||
return toast.warn('Pilih kategori dokumen');
|
||||
}
|
||||
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
})
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
});
|
||||
|
||||
const uploaded = res.data?.data;
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal mengupload file");
|
||||
if (!uploaded?.id) {
|
||||
throw new Error('Gagal mengunggah dokumen');
|
||||
}
|
||||
|
||||
stateKorupsi.create.form.fileId = uploaded.id;
|
||||
await stateKorupsi.create.create();
|
||||
|
||||
toast.success('Data berhasil disimpan');
|
||||
resetForm();
|
||||
router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
toast.error('Terjadi kesalahan saat menyimpan data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
stateKorupsi.create.form.fileId = uploaded.id;
|
||||
|
||||
await stateKorupsi.create.create();
|
||||
|
||||
resetForm();
|
||||
router.push("/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi")
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Dokumen Desa Anti Korupsi
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create Kegiatan Desa</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Document</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.pdf', '.doc', '.docx'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format document
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewFile ? (
|
||||
<iframe
|
||||
src={previewFile}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada dokumen tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<TextInput
|
||||
value={stateKorupsi.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateKorupsi.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<CreateEditor
|
||||
value={stateKorupsi.create.form.deskripsi}
|
||||
onChange={(val) => {
|
||||
stateKorupsi.create.form.deskripsi = val;
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Dokumen
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{
|
||||
'application/*': ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'],
|
||||
}}
|
||||
radius="md"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconFile size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<div>
|
||||
<Text size="lg" inline>
|
||||
Seret dokumen ke sini atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewFile && (
|
||||
<Box mt="md" style={{ textAlign: 'center' }}>
|
||||
<iframe
|
||||
src={previewFile}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<TextInput
|
||||
label="Judul Dokumen"
|
||||
placeholder="Masukkan judul dokumen"
|
||||
value={stateKorupsi.create.form.name || ''}
|
||||
onChange={(e) => (stateKorupsi.create.form.name = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<CreateEditor
|
||||
value={stateKorupsi.create.form.deskripsi || ''}
|
||||
onChange={(val) => (stateKorupsi.create.form.deskripsi = val)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Select
|
||||
value={stateKorupsi.create.form.kategoriId}
|
||||
onChange={(val) => {
|
||||
stateKorupsi.create.form.kategoriId = val ?? "";
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||
label="Kategori"
|
||||
placeholder="Pilih kategori"
|
||||
value={stateKorupsi.create.form.kategoriId || ''}
|
||||
onChange={(val) => (stateKorupsi.create.form.kategoriId = val || '')}
|
||||
data={
|
||||
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
required
|
||||
searchable
|
||||
clearable
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
<Group justify="right" mt="xl">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
loading={isLoading}
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDesaAntiKorupsi;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
|
||||
|
||||
function DesaAntiKorupsi() {
|
||||
@@ -15,9 +14,9 @@ function DesaAntiKorupsi() {
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='List Desa Anti Korupsi'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
title="Program Desa Anti Korupsi"
|
||||
placeholder="Cari nama program atau kategori..."
|
||||
searchIcon={<IconSearch size={18} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
@@ -27,114 +26,131 @@ function DesaAntiKorupsi() {
|
||||
}
|
||||
|
||||
function ListDesaAntiKorupsi({ search }: { search: string }) {
|
||||
const listState = useProxy(korupsiState.desaAntikorupsi)
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = listState.findMany;
|
||||
const listState = useProxy(korupsiState.desaAntikorupsi);
|
||||
const { data, page, totalPages, loading, load } = listState.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
// Handle loading state
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={550} />
|
||||
<Stack py="md">
|
||||
<Skeleton height={650} radius="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Desa Anti Korupsi'
|
||||
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
|
||||
/>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Kategori</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
</Table>
|
||||
</Box>
|
||||
<Box py="md">
|
||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
||||
<Stack align="center" gap="sm">
|
||||
<Title order={4}>Data Program Desa Anti Korupsi</Title>
|
||||
<Text c="dimmed" ta="center">
|
||||
Belum ada data program yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<JudulList
|
||||
title='List Desa Anti Korupsi'
|
||||
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
|
||||
/>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Desa Anti Korupsi</TableTh>
|
||||
<TableTh>Deskripsi Desa Anti Korupsi</TableTh>
|
||||
<TableTh>Kategori Desa Anti Korupsi</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<Box>
|
||||
<Stack gap="md">
|
||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Program Desa Anti Korupsi</Title>
|
||||
<Tooltip label="Tambah Program Baru" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
|
||||
withRowBorders
|
||||
verticalSpacing="sm"
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '50%' }}>Nama Program</TableTh>
|
||||
<TableTh style={{ width: '30%' }}>Kategori</TableTh>
|
||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||
</Box>
|
||||
<TableTd style={{ width: '50%' }}>
|
||||
<Text fw={500} lineClamp={1}>
|
||||
{item.name || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
<TableTd style={{ width: '30%' }}>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{item.kategori?.name || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>{item.kategori?.name}</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`)}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
<TableTd style={{ width: '20%', textAlign: 'center' }}>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
/>
|
||||
</Center>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3}>
|
||||
<Text ta="center" c="dimmed">
|
||||
Tidak ditemukan data dengan kata kunci pencarian
|
||||
</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
total={totalPages}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
size="md"
|
||||
radius="md"
|
||||
mt="md"
|
||||
/>
|
||||
</Center>
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default DesaAntiKorupsi;
|
||||
|
||||
@@ -1,63 +1,90 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { IconChartBar, IconUsers } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "Grafik Kepuasan Masyarakat",
|
||||
value: "grafikkepuasannamasyarakat",
|
||||
href: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat"
|
||||
},
|
||||
{
|
||||
label: "Responden",
|
||||
value: "responden",
|
||||
href: "/admin/landing-page/indeks-kepuasan-masyarakat/responden"
|
||||
},
|
||||
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const tabs = [
|
||||
{
|
||||
label: "Grafik Kepuasan",
|
||||
description: "Lihat visualisasi grafik kepuasan masyarakat",
|
||||
value: "grafik",
|
||||
href: "/admin/landing-page/indeks-kepuasan-masyarakat/grafik-kepuasan-masyarakat",
|
||||
icon: <IconChartBar size={18} stroke={1.8} />
|
||||
},
|
||||
{
|
||||
label: "Responden",
|
||||
description: "Kelola dan tinjau data responden",
|
||||
value: "responden",
|
||||
href: "/admin/landing-page/indeks-kepuasan-masyarakat/responden",
|
||||
icon: <IconUsers size={18} stroke={1.8} />
|
||||
},
|
||||
];
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
const curentTab = tabs.find(tab => tab.href === pathname);
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
}
|
||||
}, [pathname])
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value);
|
||||
if (tab) router.push(tab.href);
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Indeks Kepuasan Masyarakat</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname);
|
||||
if (match) setActiveTab(match.value);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Title order={2} style={{ fontWeight: 700, color: "#1a1a1a" }}>
|
||||
Indeks Kepuasan Masyarakat
|
||||
</Title>
|
||||
<Tabs
|
||||
radius="xl"
|
||||
color={colors['blue-button']}
|
||||
variant="pills"
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "#F3F4FB",
|
||||
borderRadius: "1rem",
|
||||
|
||||
}}
|
||||
>
|
||||
{tabs.map((e, i) => (
|
||||
<Tooltip key={i} label={e.description} withArrow position="bottom" transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||
<TabsTab
|
||||
value={e.value}
|
||||
leftSection={e.icon}
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{e.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value} mt="md">
|
||||
<></>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabsKepuasan;
|
||||
export default LayoutTabsKepuasan;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
import colors from '@/con/colors';
|
||||
import { PieChart, BarChart } from '@mantine/charts'; // ✅ Ganti recharts dengan Mantine
|
||||
import { PieChart, BarChart } from '@mantine/charts';
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
@@ -22,11 +22,8 @@ interface ChartDataItem {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Page() {
|
||||
const state = useProxy(indeksKepuasanState.responden);
|
||||
const { data, loading } = state.findMany;
|
||||
@@ -41,28 +38,23 @@ function Page() {
|
||||
return;
|
||||
}
|
||||
if (data) {
|
||||
// Hitung total berdasarkan jenis kelamin
|
||||
const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length;
|
||||
const totalPerempuan = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'perempuan').length;
|
||||
|
||||
// Hitung total berdasarkan rating
|
||||
const totalSangatBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat baik').length;
|
||||
const totalBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'baik').length;
|
||||
const totalKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'kurang baik').length;
|
||||
const totalSangatKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat kurang baik').length;
|
||||
|
||||
// Hitung total berdasarkan kelompok umur
|
||||
const totalMuda = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'muda').length;
|
||||
const totalDewasa = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'dewasa').length;
|
||||
const totalLansia = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'lansia').length;
|
||||
|
||||
// Update gender chart data
|
||||
setDonutDataJenisKelamin([
|
||||
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
|
||||
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
|
||||
]);
|
||||
|
||||
// Update rating chart data
|
||||
setDonutDataRating([
|
||||
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] },
|
||||
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' },
|
||||
@@ -70,18 +62,15 @@ function Page() {
|
||||
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' },
|
||||
]);
|
||||
|
||||
// Update age group chart data
|
||||
setDonutDataKelompokUmur([
|
||||
{ name: 'Muda', value: totalMuda, color: colors['blue-button'] },
|
||||
{ name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' },
|
||||
{ name: 'Lansia', value: totalLansia, color: '#FFA500' },
|
||||
]);
|
||||
|
||||
// Process data for bar chart (group by month)
|
||||
const monthYearMap = new Map<string, number>();
|
||||
|
||||
data.forEach((item: any) => {
|
||||
// Try both createdAt and tanggal fields
|
||||
const dateValue = item.tanggal || item.createdAt;
|
||||
if (!dateValue) return;
|
||||
|
||||
@@ -95,7 +84,6 @@ function Page() {
|
||||
monthYearMap.set(monthYearKey, (monthYearMap.get(monthYearKey) || 0) + 1);
|
||||
});
|
||||
|
||||
// Convert map to array and sort by date
|
||||
const barData = Array.from(monthYearMap.entries())
|
||||
.map(([key, count]) => {
|
||||
const [year, month] = key.split('-');
|
||||
@@ -104,7 +92,7 @@ function Page() {
|
||||
return {
|
||||
month: `${monthName} ${year}`,
|
||||
count,
|
||||
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
|
||||
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.sortKey - b.sortKey)
|
||||
@@ -116,28 +104,27 @@ function Page() {
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={730} />
|
||||
<Stack py="xl">
|
||||
<Skeleton height={750} radius="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan
|
||||
<Stack py="xl">
|
||||
<Text c="dimmed" ta="center" size="lg">
|
||||
Belum ada data yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
{/* Bar Chart - Data per Tanggal */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md" mb="md">
|
||||
<Title order={4} mb="md" ta="center">Jumlah Responden per Bulan</Title>
|
||||
<Box h={300}>
|
||||
<Stack gap="lg">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
|
||||
<Title order={3} mb="md" ta="center">Tren Jumlah Responden</Title>
|
||||
<Box h={320}>
|
||||
<BarChart
|
||||
h={300}
|
||||
data={barChartData}
|
||||
@@ -152,14 +139,13 @@ function Page() {
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||
{/* Chart Jenis Kelamin */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
|
||||
<Stack>
|
||||
<Title order={4}>Jenis Kelamin</Title>
|
||||
<Title order={4} ta="center">Distribusi Jenis Kelamin</Title>
|
||||
{donutDataJenisKelamin.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
Tidak ada data
|
||||
</Text>
|
||||
) : (
|
||||
<Box>
|
||||
@@ -168,15 +154,15 @@ function Page() {
|
||||
withLabels
|
||||
withTooltip
|
||||
labelsType="percent"
|
||||
size={250}
|
||||
size={220}
|
||||
data={donutDataJenisKelamin}
|
||||
/>
|
||||
</Center>
|
||||
<Stack gap="sm" mt="md">
|
||||
<Stack gap="xs" mt="md">
|
||||
{donutDataJenisKelamin.map((entry) => (
|
||||
<Flex key={entry.name} gap="md" align="center">
|
||||
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
|
||||
<Text size="sm">{entry.name}: {entry.value}</Text>
|
||||
<Flex key={entry.name} gap="sm" align="center">
|
||||
<Box bg={entry.color} w={14} h={14} style={{ borderRadius: 4, flexShrink: 0 }} />
|
||||
<Text size="sm" fw={500}>{entry.name}: {entry.value}</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Stack>
|
||||
@@ -185,13 +171,12 @@ function Page() {
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Chart Rating */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
|
||||
<Stack>
|
||||
<Title order={4}>Pilihan</Title>
|
||||
<Title order={4} ta="center">Distribusi Penilaian</Title>
|
||||
{donutDataRating.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
Tidak ada data
|
||||
</Text>
|
||||
) : (
|
||||
<Box>
|
||||
@@ -200,15 +185,15 @@ function Page() {
|
||||
withLabels
|
||||
withTooltip
|
||||
labelsType="percent"
|
||||
size={250}
|
||||
size={220}
|
||||
data={donutDataRating}
|
||||
/>
|
||||
</Center>
|
||||
<Stack gap="sm" mt="md">
|
||||
<Stack gap="xs" mt="md">
|
||||
{donutDataRating.map((entry) => (
|
||||
<Flex key={entry.name} gap="md" align="center">
|
||||
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
|
||||
<Text size="sm">{entry.name}: {entry.value}</Text>
|
||||
<Flex key={entry.name} gap="sm" align="center">
|
||||
<Box bg={entry.color} w={14} h={14} style={{ borderRadius: 4, flexShrink: 0 }} />
|
||||
<Text size="sm" fw={500}>{entry.name}: {entry.value}</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Stack>
|
||||
@@ -217,13 +202,12 @@ function Page() {
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Chart Kelompok Umur */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
|
||||
<Stack>
|
||||
<Title order={4}>Umur</Title>
|
||||
<Title order={4} ta="center">Distribusi Kelompok Umur</Title>
|
||||
{donutDataKelompokUmur.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
Tidak ada data
|
||||
</Text>
|
||||
) : (
|
||||
<Box>
|
||||
@@ -232,15 +216,15 @@ function Page() {
|
||||
withLabels
|
||||
withTooltip
|
||||
labelsType="percent"
|
||||
size={250}
|
||||
size={220}
|
||||
data={donutDataKelompokUmur}
|
||||
/>
|
||||
</Center>
|
||||
<Stack gap="sm" mt="md">
|
||||
<Stack gap="xs" mt="md">
|
||||
{donutDataKelompokUmur.map((entry) => (
|
||||
<Flex key={entry.name} gap="md" align="center">
|
||||
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
|
||||
<Text size="sm">{entry.name}: {entry.value}</Text>
|
||||
<Flex key={entry.name} gap="sm" align="center">
|
||||
<Box bg={entry.color} w={14} h={14} style={{ borderRadius: 4, flexShrink: 0 }} />
|
||||
<Text size="sm" fw={500}>{entry.name}: {entry.value}</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Stack>
|
||||
@@ -253,4 +237,4 @@ function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
|
||||
@@ -4,8 +4,8 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Stack, Title, TextInput, Text, Select } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { Box, Button, Group, Paper, Stack, Title, TextInput, Text, Select, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack, IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -74,23 +74,39 @@ function EditResponden() {
|
||||
state.update.id = id;
|
||||
state.update.form = { ...formData }; // <-- sinkronisasi manual
|
||||
await state.update.submit();
|
||||
router.push('/admin/ppid/ikm-desa-darmasaba/responden')
|
||||
router.push('/admin/landing-page/indeks-kepuasan-masyarakat/responden')
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack size={20} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
|
||||
<Stack>
|
||||
<Title order={3}>Edit Responden</Title>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Responden
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama"
|
||||
label={
|
||||
<Text fw="bold" fz="sm" mb={4}>
|
||||
Nama Responden
|
||||
</Text>
|
||||
}
|
||||
type='text'
|
||||
placeholder="masukkan nama"
|
||||
placeholder="Masukkan nama responden"
|
||||
value={formData.name}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
@@ -98,9 +114,15 @@ function EditResponden() {
|
||||
name: val.currentTarget.value
|
||||
})
|
||||
}}
|
||||
radius="md"
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
label="Tanggal"
|
||||
label={
|
||||
<Text fw="bold" fz="sm" mb={4}>
|
||||
Tanggal
|
||||
</Text>
|
||||
}
|
||||
type="date"
|
||||
placeholder='Pilih tanggal'
|
||||
value={formData.tanggal ? new Date(formData.tanggal).toISOString().split('T')[0] : ''}
|
||||
@@ -111,32 +133,43 @@ function EditResponden() {
|
||||
tanggal: selectedDate,
|
||||
});
|
||||
}}
|
||||
radius="md"
|
||||
required
|
||||
/>
|
||||
<Select
|
||||
key={"jenisKelamin"}
|
||||
label={<Text fw="bold" fz="sm">Jenis Kelamin</Text>}
|
||||
key="jenisKelamin"
|
||||
label={
|
||||
<Text fw="bold" fz="sm" mb={4}>
|
||||
Jenis Kelamin
|
||||
</Text>
|
||||
}
|
||||
placeholder="Pilih jenis kelamin"
|
||||
value={formData.jenisKelaminId}
|
||||
onChange={(val) => setFormData({ ...formData, jenisKelaminId: val || "" })}
|
||||
data={
|
||||
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
|
||||
.filter(Boolean) // Hapus null/undefined
|
||||
.filter(Boolean)
|
||||
.map((v) => ({
|
||||
value: v.id || '',
|
||||
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
|
||||
}))
|
||||
}
|
||||
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading} // ✅ disable saat loading
|
||||
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
|
||||
clearable
|
||||
searchable
|
||||
required
|
||||
radius="md"
|
||||
error={!formData.jenisKelaminId ? "Pilih jenis kelamin" : undefined}
|
||||
/>
|
||||
<Select
|
||||
key={"rating"}
|
||||
key="rating"
|
||||
value={formData.ratingId}
|
||||
onChange={(val) => setFormData({ ...formData, ratingId: val || "" })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Rating</Text>}
|
||||
label={
|
||||
<Text fw="bold" fz="sm" mb={4}>
|
||||
Rating
|
||||
</Text>
|
||||
}
|
||||
placeholder='Pilih rating'
|
||||
data={
|
||||
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
|
||||
@@ -150,6 +183,7 @@ function EditResponden() {
|
||||
clearable
|
||||
searchable
|
||||
required
|
||||
radius="md"
|
||||
error={!formData.ratingId ? "Pilih rating" : undefined}
|
||||
/>
|
||||
|
||||
@@ -173,14 +207,24 @@ function EditResponden() {
|
||||
required
|
||||
error={!formData.kelompokUmurId ? "Pilih kelompok umur" : undefined}
|
||||
/>
|
||||
|
||||
<Button
|
||||
mt={10}
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconDeviceFloppy size={20} />}
|
||||
onClick={handleSubmit}
|
||||
loading={state.update.loading}
|
||||
color={colors['blue-button']}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import { ModalKonfirmasiHapus } from "@/app/admin/(dashboard)/_com/modalKonfirmasiHapus"
|
||||
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan"
|
||||
import colors from "@/con/colors"
|
||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from "@mantine/core"
|
||||
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from "@mantine/core"
|
||||
import { useShallowEffect } from "@mantine/hooks"
|
||||
import { IconArrowBack, IconEdit, IconX } from "@tabler/icons-react"
|
||||
import { useRouter, useParams } from "next/navigation"
|
||||
import { IconArrowBack, IconEdit, IconTrash } from "@tabler/icons-react"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
import { useProxy } from "valtio/utils"
|
||||
|
||||
@@ -38,67 +38,89 @@ export default function DetailResponden() {
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail Responden</Text>
|
||||
<Box py={10}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Responden
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Nama Responden</Text>
|
||||
<Text fz={"lg"}>{stateDetail.findUnique.data?.name}</Text>
|
||||
<Text fz="lg" fw="bold">Nama Responden</Text>
|
||||
<Text fz="md" c="dimmed">{stateDetail.findUnique.data?.name || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Tanggal</Text>
|
||||
<Text fz={"lg"}>{
|
||||
<Text fz="lg" fw="bold">Tanggal</Text>
|
||||
<Text fz="md" c="dimmed">{
|
||||
stateDetail.findUnique.data?.tanggal
|
||||
? new Date(stateDetail.findUnique.data.tanggal).toLocaleDateString('id-ID')
|
||||
: '-'
|
||||
}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Jenis Kelamin</Text>
|
||||
<Text fz={"lg"}>{stateDetail.findUnique.data?.jenisKelamin?.name}</Text>
|
||||
<Text fz="lg" fw="bold">Jenis Kelamin</Text>
|
||||
<Text fz="md" c="dimmed">{stateDetail.findUnique.data?.jenisKelamin?.name || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Rating</Text>
|
||||
<Text fz={"lg"}>{stateDetail.findUnique.data?.rating?.name}</Text>
|
||||
<Text fz="lg" fw="bold">Rating</Text>
|
||||
<Text fz="md" c="dimmed">{stateDetail.findUnique.data?.rating?.name || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Kelompok Umur</Text>
|
||||
<Text fz={"lg"}>{stateDetail.findUnique.data?.kelompokUmur?.name}</Text>
|
||||
<Text fz="lg" fw="bold">Kelompok Umur</Text>
|
||||
<Text fz="md" c="dimmed">{stateDetail.findUnique.data?.kelompokUmur?.name || '-'}</Text>
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (stateDetail.findUnique.data) {
|
||||
setSelectedId(stateDetail.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
|
||||
color={"red"}
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (stateDetail.findUnique.data) {
|
||||
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${stateDetail.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!stateDetail.findUnique.data}
|
||||
color={"green"}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Group gap="sm" mt="md">
|
||||
<Tooltip label="Hapus Responden" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
if (stateDetail.findUnique.data) {
|
||||
setSelectedId(stateDetail.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
|
||||
leftSection={<IconTrash size={20} />}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Edit Responden" withArrow position="top">
|
||||
<Button
|
||||
color="green"
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
if (stateDetail.findUnique.data) {
|
||||
router.push(`/admin/landing-page/indeks-kepuasan-masyarakat/responden/${stateDetail.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!stateDetail.findUnique.data}
|
||||
leftSection={<IconEdit size={20} />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
'use client';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
|
||||
|
||||
function Responden() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Responden'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
title="Data Responden"
|
||||
placeholder="Cari nama responden..."
|
||||
searchIcon={<IconSearch size={18} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
@@ -34,84 +49,99 @@ function ListResponden({ search }: ListRespondenProps) {
|
||||
const router = useRouter();
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10)
|
||||
load(page, 10);
|
||||
}, [page]);
|
||||
|
||||
|
||||
const filteredData = (data || []).filter(item => {
|
||||
const filteredData = (data || []).filter((item) => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword)
|
||||
);
|
||||
return item.name.toLowerCase().includes(keyword);
|
||||
});
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={730} />
|
||||
<Stack py="md">
|
||||
<Skeleton height={650} radius="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper p="md">
|
||||
<Stack>
|
||||
<Title>Responden</Title>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>No</TableTh>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Tanggal</TableTh>
|
||||
<TableTh>Jenis Kelamin</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
</Table>
|
||||
<Text ta="center">Tidak ada data berdasarkan jenis kelamin responden yang tersedia</Text>
|
||||
<Box py="md">
|
||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
||||
<Stack align="center" gap="sm">
|
||||
<Title order={4}>Data Responden</Title>
|
||||
<Text c="dimmed" ta="center">
|
||||
Belum ada data responden yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box >
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap="xs">
|
||||
<Paper bg={colors['white-1']} p="md" h={{ base: 730, md: 650 }}>
|
||||
<Title mb={10} order={3}>List Responden</Title>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<Stack gap="md">
|
||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
||||
<Title order={4} mb="sm">
|
||||
Daftar Responden
|
||||
</Title>
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
verticalSpacing="sm"
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
|
||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '25%', textAlign: 'center' }}>Nama</TableTh>
|
||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Tanggal</TableTh>
|
||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jenis Kelamin</TableTh>
|
||||
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
|
||||
<TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length === 0 ? (
|
||||
<TableTr>
|
||||
<TableTd colSpan={6}>
|
||||
<Text ta='center' c='dimmed'>Belum ada data responden</Text>
|
||||
<TableTd colSpan={5}>
|
||||
<Text ta="center" c="dimmed">
|
||||
Tidak ditemukan data dengan kata kunci pencarian
|
||||
</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
) : (
|
||||
filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
|
||||
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.name}</TableTd>
|
||||
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.tanggal
|
||||
? new Date(item.tanggal).toLocaleDateString('id-ID')
|
||||
: '-'}</TableTd>
|
||||
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.jenisKelamin.name}</TableTd>
|
||||
<TableTd style={{ width: '15%', textAlign: 'center' }}>
|
||||
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)}>
|
||||
<IconDeviceImac size={20} />
|
||||
<TableTd ta="center">{index + 1}</TableTd>
|
||||
<TableTd ta="center">{item.name}</TableTd>
|
||||
<TableTd ta="center">
|
||||
{item.tanggal
|
||||
? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '-'}
|
||||
</TableTd>
|
||||
<TableTd ta="center">{item.jenisKelamin.name}</TableTd>
|
||||
<TableTd ta="center">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImac size={16} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/landing-page/indeks-kepuasan-masyarakat/responden/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
@@ -123,22 +153,19 @@ function ListResponden({ search }: ListRespondenProps) {
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
total={totalPages}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo(0, 0);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
size="md"
|
||||
radius="md"
|
||||
mt="md"
|
||||
mb="md"
|
||||
/>
|
||||
</Center>
|
||||
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Responden;
|
||||
|
||||
|
||||
|
||||
@@ -1,62 +1,105 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { IconCategory, IconListDetails } from '@tabler/icons-react';
|
||||
import colors from '@/con/colors';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "List Prestasi Desa",
|
||||
value: "listPrestasiDesa",
|
||||
href: "/admin/landing-page/prestasi-desa/list-prestasi-desa"
|
||||
},
|
||||
{
|
||||
label: "Kategori Prestasi Desa",
|
||||
value: "kategoriPrestasiDesa",
|
||||
href: "/admin/landing-page/prestasi-desa/kategori-prestasi-desa"
|
||||
},
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
}
|
||||
setActiveTab(value)
|
||||
const tabs = [
|
||||
{
|
||||
label: "Daftar Prestasi",
|
||||
value: "listPrestasiDesa",
|
||||
href: "/admin/landing-page/prestasi-desa/list-prestasi-desa",
|
||||
icon: <IconListDetails size={18} stroke={1.8} />,
|
||||
tooltip: "Kelola daftar prestasi desa",
|
||||
},
|
||||
{
|
||||
label: "Kategori Prestasi",
|
||||
value: "kategoriPrestasiDesa",
|
||||
href: "/admin/landing-page/prestasi-desa/kategori-prestasi-desa",
|
||||
icon: <IconCategory size={18} stroke={1.8} />,
|
||||
tooltip: "Kelola kategori prestasi desa",
|
||||
},
|
||||
];
|
||||
|
||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value);
|
||||
if (tab) {
|
||||
router.push(tab.href);
|
||||
}
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
}
|
||||
}, [pathname])
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname);
|
||||
if (match) {
|
||||
setActiveTab(match.value);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Prestasi Desa</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||
Prestasi Desa
|
||||
</Title>
|
||||
<Tabs
|
||||
color={colors['blue-button']}
|
||||
variant="pills"
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
key={i}
|
||||
value={tab.value}
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabs;
|
||||
@@ -2,7 +2,7 @@
|
||||
'use client'
|
||||
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -45,7 +45,7 @@ function EditKategoriPrestasi() {
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (!formData.name.trim()) {
|
||||
toast.error('Nama kategori prestasi desa tidak boleh kosong');
|
||||
toast.error('Nama kategori prestasi tidak boleh kosong');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,24 +70,48 @@ function EditKategoriPrestasi() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Kategori Prestasi
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Edit Kategori Prestasi Desa</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Kategori Prestasi"
|
||||
placeholder="Masukkan nama kategori prestasi"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Prestasi Desa</Text>}
|
||||
placeholder='Masukkan nama kategori prestasi desa'
|
||||
required
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
'use client'
|
||||
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
@@ -30,31 +30,51 @@ function CreateKategoriPrestasi() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Kategori Prestasi
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create Kategori Prestasi Desa</Title>
|
||||
<TextInput
|
||||
value={stateKategori.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateKategori.create.form.name = val.target.value;
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Kategori Prestasi"
|
||||
placeholder="Masukkan nama kategori prestasi"
|
||||
value={stateKategori.create.form.name || ''}
|
||||
onChange={(val) => (stateKategori.create.form.name = val.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Prestasi Desa</Text>}
|
||||
placeholder='Masukkan nama kategori prestasi desa'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import prestasiState from '../../../_state/landing-page/prestasi-desa';
|
||||
|
||||
@@ -18,7 +17,7 @@ function KategoriPrestasiDesa() {
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Kategori Prestasi Desa'
|
||||
placeholder='pencarian'
|
||||
placeholder='Cari kategori prestasi...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
@@ -65,60 +64,100 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Kategori Prestasi Desa'
|
||||
href='/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create'
|
||||
/>
|
||||
<Box style={{ overflowY: "auto" }}>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<Box>
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm" withBorder>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4} c="dark">List Kategori Prestasi</Title>
|
||||
<Tooltip label="Tambah Kategori Prestasi" withArrow>
|
||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table verticalSpacing="sm" highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Kategori</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
<TableTh style={{ width: '120px' }} ta={'center'}>Edit</TableTh>
|
||||
<TableTh ta={'center'} style={{ width: '120px' }}>Delete</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
<Button color="green" onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button color="red" onClick={() => {
|
||||
setSelectedId(item.id)
|
||||
setModalHapus(true)
|
||||
}}>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
{filteredData.length === 0 ? (
|
||||
<TableTr>
|
||||
<TableTd colSpan={2} style={{ textAlign: 'center' }}>
|
||||
<Text py="md" c="dimmed">
|
||||
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
) : (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd style={{ textAlign: 'center', width: '120px' }}>
|
||||
<Tooltip label="Edit" withArrow position="top">
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
<TableTd style={{ textAlign: 'center', width: '120px' }}>
|
||||
<Tooltip label="Hapus" withArrow position="top">
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={18} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
withEdges
|
||||
size="sm"
|
||||
styles={{
|
||||
control: {
|
||||
'&[data-active]': {
|
||||
background: `${colors['blue-button']} !important`,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Paper>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
my={"md"}
|
||||
/>
|
||||
</Center>
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus kategori prestasi desa ini?'
|
||||
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
|
||||
/>
|
||||
</Box>
|
||||
</Box >
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
'use client'
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -109,122 +109,144 @@ function EditPrestasiDesa() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Edit List Prestasi Desa</Text>
|
||||
{editState.findUnique.data ? (
|
||||
<Paper key={editState.findUnique.data.id}>
|
||||
<Stack gap={"xs"}>
|
||||
<TextInput
|
||||
value={formData.name}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
name: val.target.value
|
||||
})
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
deskripsi: val
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Select
|
||||
value={formData.kategoriId}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
kategoriId: val ?? ""
|
||||
})
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||
placeholder="Pilih kategori"
|
||||
data={
|
||||
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</Text>
|
||||
<Stack gap={"xs"}>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconFile size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Prestasi Desa
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Image</Text>
|
||||
{previewFile ? (
|
||||
<Image
|
||||
alt=''
|
||||
src={previewFile}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Judul Prestasi"
|
||||
placeholder="Masukkan judul prestasi"
|
||||
value={formData.name}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
name: val.target.value
|
||||
});
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
deskripsi: val
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Select
|
||||
label="Kategori"
|
||||
placeholder="Pilih kategori"
|
||||
value={formData.kategoriId}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
kategoriId: val ?? ""
|
||||
});
|
||||
}}
|
||||
data={
|
||||
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Prestasi
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Stack gap="xs" align="center">
|
||||
<Text size="md" fw={500}>
|
||||
Seret gambar atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Maksimal 5MB, format gambar wajib
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewFile && (
|
||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Image
|
||||
src={previewFile}
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default EditPrestasiDesa;
|
||||
@@ -2,9 +2,9 @@
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -40,45 +40,69 @@ function DetailPrestasiDesa() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail List Prestasi Desa</Text>
|
||||
{detailState.findUnique.data ? (
|
||||
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Judul</Text>
|
||||
<Text fz={"lg"}>{detailState.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi }} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Kategori</Text>
|
||||
<Text fz={"lg"}>{detailState.findUnique.data?.kategori?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Image</Text>
|
||||
{detailState.findUnique.data?.image?.link ? (
|
||||
<iframe
|
||||
src={detailState.findUnique.data.image.link}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Box py={10}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Prestasi Desa
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Judul</Text>
|
||||
<Text fz="md" c="dimmed">{detailState.findUnique.data?.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Kategori</Text>
|
||||
<Text fz="md" c="dimmed">{detailState.findUnique.data?.kategori?.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||
<Box
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi || '-' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Gambar</Text>
|
||||
{detailState.findUnique.data?.image?.link ? (
|
||||
<Image
|
||||
src={detailState.findUnique.data.image.link}
|
||||
alt={detailState.findUnique.data.name || 'Gambar Prestasi'}
|
||||
w={300}
|
||||
fit="contain"
|
||||
style={{ borderRadius: '8px', border: '1px solid #e0e0e0' }}
|
||||
/>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group gap="sm">
|
||||
<Tooltip label="Hapus Prestasi" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
if (detailState.findUnique.data) {
|
||||
setSelectedId(detailState.findUnique.data.id);
|
||||
@@ -86,25 +110,33 @@ function DetailPrestasiDesa() {
|
||||
}
|
||||
}}
|
||||
disabled={detailState.delete.loading || !detailState.findUnique.data}
|
||||
color={"red"}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconX size={20} />
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Edit Prestasi" withArrow position="top">
|
||||
<Button
|
||||
color="green"
|
||||
onClick={() => {
|
||||
if (detailState.findUnique.data) {
|
||||
router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${detailState.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!detailState.findUnique.data}
|
||||
color={"green"}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -60,103 +59,132 @@ function CreatePrestasiDesa() {
|
||||
router.push("/admin/landing-page/prestasi-desa/list-prestasi-desa")
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Prestasi Desa
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create Prestasi Desa</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.jpg', '.jpeg', '.png'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Prestasi
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewFile(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid. Hanya file gambar yang diperbolehkan.')}
|
||||
maxSize={5 * 1024 ** 2} // 5MB
|
||||
accept={{
|
||||
'image/*': ['.jpg', '.jpeg', '.png', '.webp'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Image</Text>
|
||||
{previewFile ? (
|
||||
<iframe
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Seret file gambar ke sini atau klik untuk memilih
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Unggah file gambar (maks. 5MB)
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewFile && (
|
||||
<Box mt="md">
|
||||
<Text size="sm" fw={500} mb={4}>
|
||||
Pratinjau Gambar:
|
||||
</Text>
|
||||
<Box style={{ maxWidth: '100%', maxHeight: '300px', overflow: 'hidden', borderRadius: '8px' }}>
|
||||
<Image
|
||||
src={previewFile}
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
alt="Pratinjau gambar prestasi"
|
||||
fit="cover"
|
||||
style={{ width: '100%', height: 'auto' }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<TextInput
|
||||
label="Judul Prestasi"
|
||||
placeholder="Masukkan judul prestasi"
|
||||
value={stateCreate.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateCreate.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
onChange={(e) => (stateCreate.create.form.name = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<Text fw={500} fz="sm" mb={6}>
|
||||
Deskripsi Prestasi
|
||||
</Text>
|
||||
<CreateEditor
|
||||
value={stateCreate.create.form.deskripsi}
|
||||
onChange={(val) => {
|
||||
stateCreate.create.form.deskripsi = val;
|
||||
}}
|
||||
onChange={(val) => (stateCreate.create.form.deskripsi = val)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Select
|
||||
value={stateCreate.create.form.kategoriId}
|
||||
onChange={(val) => {
|
||||
stateCreate.create.form.kategoriId = val ?? "";
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Kategori</Text>}
|
||||
label="Kategori Prestasi"
|
||||
placeholder="Pilih kategori"
|
||||
value={stateCreate.create.form.kategoriId}
|
||||
onChange={(val) => (stateCreate.create.form.kategoriId = val ?? '')}
|
||||
data={
|
||||
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.name,
|
||||
})) || []
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="light"
|
||||
color="gray"
|
||||
onClick={() => router.back()}
|
||||
style={{ marginRight: 'auto' }}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
bg={colors['blue-button']}
|
||||
style={{ boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)' }}
|
||||
>
|
||||
Simpan Prestasi
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import prestasiState from '../../../_state/landing-page/prestasi-desa';
|
||||
|
||||
|
||||
function ListPrestasiDesa() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='List Prestasi Desa'
|
||||
placeholder='pencarian'
|
||||
title='Prestasi Desa'
|
||||
placeholder='Cari nama prestasi...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
@@ -31,7 +29,7 @@ function ListPrestasi({ search }: { search: string }) {
|
||||
const listState = useProxy(prestasiState.prestasiDesa)
|
||||
const router = useRouter();
|
||||
|
||||
const{
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
@@ -62,63 +60,88 @@ function ListPrestasi({ search }: { search: string }) {
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<JudulList
|
||||
title='List Prestasi Desa'
|
||||
href='/admin/landing-page/prestasi-desa/list-prestasi-desa/create'
|
||||
/>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Prestasi Desa</TableTh>
|
||||
<TableTh>Deskripsi Prestasi Desa</TableTh>
|
||||
<TableTh>Kategori Prestasi Desa</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Prestasi Desa</Title>
|
||||
<Tooltip label="Tambah Prestasi" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '25%' }}>Nama Prestasi</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
|
||||
<TableTh style={{ width: '25%', textAlign: 'center' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd >
|
||||
<Box w={150}>
|
||||
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</Box>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
</TableTd>
|
||||
<TableTd>{item.kategori?.name || 'No Category'}</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
</Button>
|
||||
<TableTd style={{ width: '25%' }}>
|
||||
<Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '25%', textAlign: 'center' }}>
|
||||
<Tooltip label="Kelola Prestasi" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
|
||||
size="sm"
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4} style={{ textAlign: 'center' }}>
|
||||
<Text c="dimmed" py="md">Tidak ada data prestasi</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
my={"md"}
|
||||
/>
|
||||
</Center>
|
||||
{totalPages > 1 && (
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={load}
|
||||
total={totalPages}
|
||||
withEdges
|
||||
size="sm"
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,67 +1,112 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { IconBulb, IconUsers, IconBrandFacebook } from '@tabler/icons-react';
|
||||
import colors from '@/con/colors';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "Program Inovasi",
|
||||
value: "program-inovasi",
|
||||
href: "/admin/landing-page/profile/program-inovasi"
|
||||
href: "/admin/landing-page/profile/program-inovasi",
|
||||
icon: <IconBulb size={18} stroke={1.8} />,
|
||||
tooltip: "Lihat dan kelola program inovasi desa",
|
||||
},
|
||||
{
|
||||
label: "Pejabat Desa",
|
||||
value: "pejabat-desa",
|
||||
href: "/admin/landing-page/profile/pejabat-desa"
|
||||
href: "/admin/landing-page/profile/pejabat-desa",
|
||||
icon: <IconUsers size={18} stroke={1.8} />,
|
||||
tooltip: "Kelola data pejabat desa",
|
||||
},
|
||||
{
|
||||
label: "Media Sosial",
|
||||
value: "media-sosial",
|
||||
href: "/admin/landing-page/profile/media-sosial"
|
||||
href: "/admin/landing-page/profile/media-sosial",
|
||||
icon: <IconBrandFacebook size={18} stroke={1.8} />,
|
||||
tooltip: "Atur tautan media sosial desa",
|
||||
},
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
|
||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
const tab = tabs.find(t => t.value === value);
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
router.push(tab.href);
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
const match = tabs.find(tab => tab.href === pathname);
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
setActiveTab(match.value);
|
||||
}
|
||||
}, [pathname])
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Profile</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
<Stack gap="lg">
|
||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||
Profil Desa
|
||||
</Title>
|
||||
<Tabs
|
||||
color={colors['blue-button']}
|
||||
variant='pills'
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
key={i}
|
||||
value={tab.value}
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabs;
|
||||
export default LayoutTabs;
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -12,17 +23,17 @@ import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function EditMediaSosial() {
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: stateMediaSosial.update.form.name || "",
|
||||
iconUrl: stateMediaSosial.update.form.iconUrl || "",
|
||||
imageId: stateMediaSosial.update.form.imageId || ""
|
||||
})
|
||||
name: stateMediaSosial.update.form.name || '',
|
||||
iconUrl: stateMediaSosial.update.form.iconUrl || '',
|
||||
imageId: stateMediaSosial.update.form.imageId || '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const id = params?.id as string;
|
||||
@@ -34,136 +45,147 @@ function EditMediaSosial() {
|
||||
|
||||
if (data) {
|
||||
setFormData({
|
||||
name: data.name || "",
|
||||
iconUrl: data.iconUrl || "",
|
||||
imageId: data.imageId || "",
|
||||
name: data.name || '',
|
||||
iconUrl: data.iconUrl || '',
|
||||
imageId: data.imageId || '',
|
||||
});
|
||||
// Tampilkan preview gambar
|
||||
if (data.image?.link) {
|
||||
setPreviewImage(data.image.link);
|
||||
}
|
||||
if (data.image?.link) setPreviewImage(data.image.link);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading program inovasi:", error);
|
||||
console.error('Error loading media sosial:', error);
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Gagal mengambil data program inovasi"
|
||||
error instanceof Error ? error.message : 'Gagal mengambil data media sosial'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadMediaSosial();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
stateMediaSosial.update.form = {
|
||||
...stateMediaSosial.update.form,
|
||||
name: formData.name,
|
||||
iconUrl: formData.iconUrl,
|
||||
imageId: formData.imageId ?? "",
|
||||
}
|
||||
stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
|
||||
|
||||
if (file) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
}
|
||||
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||
|
||||
// Update imageId in global state
|
||||
stateMediaSosial.update.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
await stateMediaSosial.update.update();
|
||||
toast.success("Media Sosial berhasil diperbarui!");
|
||||
router.push("/admin/landing-page/profile/media-sosial");
|
||||
toast.success('Media sosial berhasil diperbarui!');
|
||||
router.push('/admin/landing-page/profile/media-sosial');
|
||||
} catch (error) {
|
||||
console.error("Error updating media sosial:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui media sosial");
|
||||
console.error('Error updating media sosial:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui media sosial');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Media Sosial
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Edit Media Sosial</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{ 'image/*': [] }}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Media Sosial
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Stack gap="xs" align="center">
|
||||
<Text size="md" fw={500}>
|
||||
Seret gambar atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Maksimal 5MB, format gambar wajib
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag gambar ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format gambar
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{/* Tampilkan preview kalau ada */}
|
||||
{previewImage && (
|
||||
<Box mt="sm">
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
{previewImage && (
|
||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<TextInput
|
||||
label="Nama Media Sosial / Kontak"
|
||||
placeholder="Masukkan nama media sosial atau kontak"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>}
|
||||
placeholder='Masukkan nama media sosial'
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Link Media Sosial / Nomor Telepon"
|
||||
placeholder="Masukkan link media sosial atau nomor telepon"
|
||||
value={formData.iconUrl}
|
||||
onChange={(e) => setFormData({ ...formData, iconUrl: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Icon URL / No Telephone</Text>}
|
||||
placeholder='Masukkan icon url'
|
||||
required
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -2,103 +2,133 @@
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function DetailMediaSosial() {
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const params = useParams()
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateMediaSosial.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
stateMediaSosial.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
stateMediaSosial.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/landing-page/profile/media-sosial")
|
||||
stateMediaSosial.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push("/admin/landing-page/profile/media-sosial");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!stateMediaSosial.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = stateMediaSosial.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail Media Sosial</Text>
|
||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box py={10}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Media Sosial
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Nama Media Sosial / Nama Kontak</Text>
|
||||
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.name}</Text>
|
||||
<Text fz="lg" fw="bold">Nama Media Sosial / Kontak</Text>
|
||||
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Icon URL / No Telephone</Text>
|
||||
<Text fz={"lg"}>{stateMediaSosial.findUnique.data?.iconUrl}</Text>
|
||||
<Text fz="lg" fw="bold">Icon / Nomor Telepon</Text>
|
||||
<Text fz="md" c="dimmed">{data.iconUrl || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
|
||||
<Box w={100} h={100}>
|
||||
<Image src={stateMediaSosial.findUnique.data?.image?.link} alt="gambar" />
|
||||
</Box>
|
||||
<Text fz="lg" fw="bold">Gambar</Text>
|
||||
{data.image?.link ? (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.name || 'Gambar Media Sosial'}
|
||||
w={120}
|
||||
h={120}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
/>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={"xs"}>
|
||||
|
||||
<Group gap="sm">
|
||||
<Tooltip label="Hapus Media Sosial" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
if (stateMediaSosial.findUnique.data) {
|
||||
setSelectedId(stateMediaSosial.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
disabled={!stateMediaSosial.findUnique.data}
|
||||
color="red">
|
||||
<IconX size={20} />
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Edit Media Sosial" withArrow position="top">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (stateMediaSosial.findUnique.data) {
|
||||
router.push(`/admin/landing-page/profile/media-sosial/${stateMediaSosial.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!stateMediaSosial.findUnique.data}
|
||||
color="green">
|
||||
color="green"
|
||||
onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${data.id}/edit`)}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text="Apakah anda yakin ingin menghapus media sosial ini?"
|
||||
text="Apakah Anda yakin ingin menghapus media sosial ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -11,9 +22,9 @@ import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
||||
|
||||
function CreateMediaSosial() {
|
||||
export default function CreateMediaSosial() {
|
||||
const router = useRouter();
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
@@ -23,27 +34,28 @@ function CreateMediaSosial() {
|
||||
|
||||
const resetForm = () => {
|
||||
stateMediaSosial.create.form = {
|
||||
name: "",
|
||||
imageId: "",
|
||||
iconUrl: "",
|
||||
name: '',
|
||||
imageId: '',
|
||||
iconUrl: '',
|
||||
};
|
||||
setPreviewImage(null);
|
||||
setFile(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!file) {
|
||||
return toast.warn("Pilih file gambar terlebih dahulu");
|
||||
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||
}
|
||||
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
})
|
||||
});
|
||||
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal mengupload file");
|
||||
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||
}
|
||||
|
||||
stateMediaSosial.create.form.imageId = uploaded.id;
|
||||
@@ -51,98 +63,108 @@ function CreateMediaSosial() {
|
||||
await stateMediaSosial.create.create();
|
||||
|
||||
resetForm();
|
||||
router.push("/admin/landing-page/profile/media-sosial")
|
||||
}
|
||||
router.push('/admin/landing-page/profile/media-sosial');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Media Sosial
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create Media Sosial</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{ 'image/*': [] }}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Media Sosial
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
</Group>
|
||||
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||
</Text>
|
||||
</Dropzone>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag gambar ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format gambar
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{/* Tampilkan preview kalau ada */}
|
||||
{previewImage && (
|
||||
<Box mt="sm">
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
{previewImage && (
|
||||
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<TextInput
|
||||
label="Nama Media Sosial / Kontak"
|
||||
placeholder="Masukkan nama media sosial atau kontak"
|
||||
value={stateMediaSosial.create.form.name || ''}
|
||||
onChange={(val) => {
|
||||
stateMediaSosial.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Media Sosial / Nama Kontak</Text>}
|
||||
placeholder='Masukkan nama media sosial / nama kontak'
|
||||
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
label="Link Media Sosial / Nomor Telepon"
|
||||
placeholder="Masukkan link media sosial atau nomor telepon"
|
||||
value={stateMediaSosial.create.form.iconUrl || ''}
|
||||
onChange={(val) => {
|
||||
stateMediaSosial.create.form.iconUrl = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Link Media Sosial / No Telephone</Text>}
|
||||
placeholder='Masukkan link media sosial / no telephone'
|
||||
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateMediaSosial;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import profileLandingPageState from '../../../_state/landing-page/profile';
|
||||
|
||||
function MediaSosial() {
|
||||
@@ -16,7 +15,7 @@ function MediaSosial() {
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Media Sosial'
|
||||
placeholder='pencarian'
|
||||
placeholder='Cari nama media sosial atau kontak...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
@@ -44,80 +43,77 @@ function ListMediaSosial({ search }: { search: string }) {
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
// Handle loading state
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={550} />
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Media Sosial'
|
||||
href='/admin/landing-page/profile/media-sosial/create'
|
||||
/>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
|
||||
<TableTh>Image</TableTh>
|
||||
<TableTh>Icon URL / No Telephone</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Media Sosial'
|
||||
href='/admin/landing-page/profile/media-sosial/create'
|
||||
/>
|
||||
<Box style={{ overflowY: "auto" }}>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Media Sosial</Title>
|
||||
<Tooltip label="Tambah Media Sosial" withArrow>
|
||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profile/media-sosial/create')}>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
|
||||
<TableTh>Image</TableTh>
|
||||
<TableTh>Icon URL / No Telephone</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
<TableTh style={{ width: '25%' }}>Nama Media Sosial / Kontak</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Gambar</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Icon / No. Telepon</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>
|
||||
<Box w={50} h={50}>
|
||||
<Image src={item.image?.link} alt={item.name} />
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={250}>
|
||||
<a style={{color: "black"}} href={item.iconUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Text truncate fz={'sm'}>{item.iconUrl}</Text>
|
||||
</a>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '25%', }}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%', }}>
|
||||
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden', }}>
|
||||
{item.image?.link ? (
|
||||
<Image src={item.image.link} alt={item.name} fit="cover" />
|
||||
) : (
|
||||
<Box bg={colors['blue-button']} w="100%" h="100%" />
|
||||
)}
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%', }}>
|
||||
<Text truncate fz="sm" c="dimmed">
|
||||
{item.iconUrl || item.noTelp || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '15%'}}>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/landing-page/profile/media-sosial/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImac size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Tidak ada data media sosial yang cocok</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
@@ -127,11 +123,13 @@ function ListMediaSosial({ search }: { search: string }) {
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo(0, 0);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
@@ -144,124 +144,134 @@ function EditPejabatDesa() {
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap="xs">
|
||||
<Box>
|
||||
<Button variant="subtle" onClick={handleBack}>
|
||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Pejabat Desa
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Box>
|
||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius={10}>
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Edit Profile Pejabat Desa</Title>
|
||||
<Paper
|
||||
w={{ base: "100%", md: "50%" }}
|
||||
bg={colors['white-1']}
|
||||
p="md"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Edit Profile Pejabat Desa</Title>
|
||||
|
||||
{/* Nama Field */}
|
||||
<TextInput
|
||||
label={<Text fw="bold">Nama Perbekel</Text>}
|
||||
placeholder="Masukkan nama perbekel"
|
||||
value={allState.edit.form.name}
|
||||
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
|
||||
error={!allState.edit.form.name && "Nama wajib diisi"}
|
||||
/>
|
||||
{/* Nama Field */}
|
||||
<TextInput
|
||||
label={<Text fw="bold">Nama Perbekel</Text>}
|
||||
placeholder="Masukkan nama perbekel"
|
||||
value={allState.edit.form.name}
|
||||
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
|
||||
error={!allState.edit.form.name && "Nama wajib diisi"}
|
||||
/>
|
||||
|
||||
{/* Posisi Field */}
|
||||
<TextInput
|
||||
label={<Text fw="bold">Posisi</Text>}
|
||||
placeholder="Masukkan posisi"
|
||||
value={allState.edit.form.position}
|
||||
onChange={(e) => handleFieldChange('position', e.currentTarget.value)}
|
||||
error={!allState.edit.form.position && "Posisi wajib diisi"}
|
||||
/>
|
||||
{/* Posisi Field */}
|
||||
<TextInput
|
||||
label={<Text fw="bold">Posisi</Text>}
|
||||
placeholder="Masukkan posisi"
|
||||
value={allState.edit.form.position}
|
||||
onChange={(e) => handleFieldChange('position', e.currentTarget.value)}
|
||||
error={!allState.edit.form.position && "Posisi wajib diisi"}
|
||||
/>
|
||||
|
||||
{/* File Upload */}
|
||||
{/* File Upload */}
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => handleFileChange(files[0])}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{ 'image/*': [] }}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Dropzone
|
||||
onDrop={(files) => handleFileChange(files[0])}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{ 'image/*': [] }}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag gambar ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format gambar
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag gambar ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format gambar
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{/* Tampilkan preview kalau ada */}
|
||||
{previewImage && (
|
||||
<Box mt="sm">
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Preview Gambar */}
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
|
||||
{previewImage ? (
|
||||
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
|
||||
) : (
|
||||
<Center w={200} h={200} bg="gray.2">
|
||||
<Stack align="center" gap="xs">
|
||||
<IconImageInPicture size={48} color="gray" />
|
||||
<Text size="sm" c="gray">Tidak ada gambar</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
{/* Tampilkan preview kalau ada */}
|
||||
{previewImage && (
|
||||
<Box mt="sm">
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
loading={isSubmitting || allState.edit.loading}
|
||||
disabled={!allState.edit.form.name}
|
||||
>
|
||||
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||
</Button>
|
||||
{/* Preview Gambar */}
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
|
||||
{previewImage ? (
|
||||
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
|
||||
) : (
|
||||
<Center w={200} h={200} bg="gray.2">
|
||||
<Stack align="center" gap="xs">
|
||||
<IconImageInPicture size={48} color="gray" />
|
||||
<Text size="sm" c="gray">Tidak ada gambar</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleBack}
|
||||
disabled={isSubmitting || allState.edit.loading}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
{/* Submit Button */}
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
loading={isSubmitting || allState.edit.loading}
|
||||
disabled={!allState.edit.form.name}
|
||||
>
|
||||
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleBack}
|
||||
disabled={isSubmitting || allState.edit.loading}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||
|
||||
|
||||
function Page() {
|
||||
const router = useRouter()
|
||||
const allList = useProxy(profileLandingPageState.pejabatDesa)
|
||||
const router = useRouter();
|
||||
const allList = useProxy(profileLandingPageState.pejabatDesa);
|
||||
|
||||
useShallowEffect(() => {
|
||||
allList.findUnique.load("edit") // Assuming "1" is your default ID, adjust as needed
|
||||
}, [])
|
||||
allList.findUnique.load("edit");
|
||||
}, []);
|
||||
|
||||
if (!allList.findUnique.data) {
|
||||
return <Stack>
|
||||
<Skeleton radius={10} h={800} />
|
||||
</Stack>
|
||||
return (
|
||||
<Stack align="center" justify="center" py="xl">
|
||||
<Skeleton radius="md" height={800} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const dataArray = Array.isArray(allList.findUnique.data)
|
||||
@@ -26,79 +28,82 @@ function Page() {
|
||||
: [allList.findUnique.data];
|
||||
|
||||
return (
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Grid>
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3}>Preview Pejabat Desa</Title>
|
||||
<Title order={3} c={colors['blue-button']}>Preview Pejabat Desa</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
<Tooltip label="Edit Profil Pejabat" withArrow>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/landing-page/profile/pejabat-desa/${allList.findUnique.data?.id}`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
{dataArray.map((item) => (
|
||||
<Box key={item.id} >
|
||||
<Paper p={"xl"} bg={colors['BG-trans']}>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 12 }}>
|
||||
<Center>
|
||||
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
|
||||
</Center>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 12 }}>
|
||||
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Divider my={"md"} color={colors['blue-button']} />
|
||||
{/* biodata perbekel */}
|
||||
<Box px={{ base: 0, md: 50 }} pb={30}>
|
||||
<Box pb={20} px={{ base: 0, md: 50 }}>
|
||||
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}>
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image
|
||||
pt={{ base: 0, md: 90 }}
|
||||
src={item.image?.link || "/perbekel.png"}
|
||||
w={{ base: 250, md: 350 }}
|
||||
alt='Foto Profil PPID'
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = "/perbekel.png";
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py={20}
|
||||
className="glass3"
|
||||
px={{ base: 10, md: 10 }}
|
||||
|
||||
>
|
||||
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Stack>
|
||||
<Paper key={item.id} p="xl" bg={'white'} withBorder radius="md" shadow="xs">
|
||||
<Box px={{ base: "sm", md: 100 }}>
|
||||
<Grid>
|
||||
<GridCol span={12}>
|
||||
<Center>
|
||||
<Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" />
|
||||
</Center>
|
||||
</GridCol>
|
||||
<GridCol span={12}>
|
||||
<Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
|
||||
Profil Pimpinan Badan Publik Desa Darmasaba
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
<Box px={{ base: 0, md: 50 }} pb="xl">
|
||||
<Paper bg={colors['BG-trans']} radius="md" shadow="xs" p="lg">
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
<Image
|
||||
pt={{ base: 0, md: 60 }}
|
||||
src={item.image?.link || "/perbekel.png"}
|
||||
w={{ base: 250, md: 350 }}
|
||||
alt="Foto Profil Pejabat"
|
||||
radius="md"
|
||||
onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
|
||||
/>
|
||||
</Center>
|
||||
<Paper
|
||||
bg={colors['blue-button']}
|
||||
py="md"
|
||||
px="sm"
|
||||
radius="md"
|
||||
className="glass3"
|
||||
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
|
||||
>
|
||||
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Paper>
|
||||
</Box>
|
||||
<Box pt={10}>
|
||||
<Box>
|
||||
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Position</Text>
|
||||
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"}>{item.position}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Box mt="lg">
|
||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Jabatan</Text>
|
||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']}>
|
||||
{item.position}
|
||||
</Text>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -86,92 +98,116 @@ function EditProgramInovasi() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Program Inovasi
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Edit Program Inovasi</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{ 'image/*': [] }}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Program Inovasi
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Stack gap="xs" align="center">
|
||||
<Text size="md" fw={500}>
|
||||
Seret gambar atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Maksimal 5MB, format gambar wajib
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag gambar ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format gambar
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{/* Tampilkan preview kalau ada */}
|
||||
{previewImage && (
|
||||
<Box mt="sm">
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
{previewImage && (
|
||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<TextInput
|
||||
label="Nama Program Inovasi"
|
||||
placeholder="Masukkan nama program inovasi"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
|
||||
placeholder='Masukkan nama produk'
|
||||
/>
|
||||
<TextInput
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
|
||||
placeholder='Masukkan deskripsi'
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<EditEditor
|
||||
value={formData.description}
|
||||
onChange={(htmlContent) => {
|
||||
setFormData((prev) => ({ ...prev, description: htmlContent }));
|
||||
stateProgramInovasi.update.form.description = htmlContent;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<TextInput
|
||||
label="Link Program Inovasi"
|
||||
placeholder="Masukkan link program inovasi (opsional)"
|
||||
value={formData.link}
|
||||
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Link</Text>}
|
||||
placeholder='Masukkan link'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -31,91 +31,117 @@ function DetailProgramInovasi() {
|
||||
|
||||
if (!stateProgramInovasi.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
<Stack py={12}>
|
||||
<Skeleton height={520} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const data = stateProgramInovasi.findUnique.data
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail Program Inovasi</Text>
|
||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box px={{ base: 'md', md: 'xl' }} py="lg">
|
||||
<Button variant="subtle" onClick={() => router.back()} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Program Inovasi
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Nama Program Inovasi</Text>
|
||||
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.name}</Text>
|
||||
<Text fz="lg" fw="bold">Nama Program Inovasi</Text>
|
||||
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
|
||||
<Text
|
||||
fz={"lg"}
|
||||
|
||||
>{stateProgramInovasi.findUnique.data?.description}</Text>
|
||||
<Text fz="lg" fw="bold">Gambar</Text>
|
||||
{data.image?.link ? (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt="Gambar Program"
|
||||
radius="md"
|
||||
style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain' }}
|
||||
/>
|
||||
) : (
|
||||
<Text fz="md" c="dimmed">-</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Link</Text>
|
||||
<a
|
||||
href={stateProgramInovasi.findUnique.data?.link || "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
wordWrap: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowWrap: 'break-word',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{stateProgramInovasi.findUnique.data?.link || "Tidak ada link"}
|
||||
</a>
|
||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||
<Text fz="md" c="dimmed" style={{ whiteSpace: 'pre-wrap' }} dangerouslySetInnerHTML={{ __html: data.description || '-' }}></Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
|
||||
<Image src={stateProgramInovasi.findUnique.data?.image?.link} alt="gambar" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex gap={"xs"}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (stateProgramInovasi.findUnique.data) {
|
||||
setSelectedId(stateProgramInovasi.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
<Text fz="lg" fw="bold">Link</Text>
|
||||
{data.link ? (
|
||||
<a
|
||||
href={data.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
color: colors['blue-button'],
|
||||
textDecoration: 'underline',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
disabled={!stateProgramInovasi.findUnique.data}
|
||||
color="red">
|
||||
<IconX size={20} />
|
||||
>
|
||||
{data.link}
|
||||
</a>
|
||||
) : (
|
||||
<Text fz="md" c="dimmed">-</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group gap="sm">
|
||||
<Tooltip label="Hapus Program Inovasi" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Edit Program Inovasi" withArrow position="top">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (stateProgramInovasi.findUnique.data) {
|
||||
router.push(`/admin/landing-page/profile/program-inovasi/${stateProgramInovasi.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!stateProgramInovasi.findUnique.data}
|
||||
color="green">
|
||||
color="green"
|
||||
onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${data.id}/edit`)}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text="Apakah anda yakin ingin menghapus program inovasi ini?"
|
||||
text="Apakah Anda yakin ingin menghapus program inovasi ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -13,7 +25,7 @@ import profileLandingPageState from '../../../../_state/landing-page/profile';
|
||||
|
||||
function CreateProgramInovasi() {
|
||||
const router = useRouter();
|
||||
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
|
||||
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
@@ -31,20 +43,21 @@ function CreateProgramInovasi() {
|
||||
setPreviewImage(null);
|
||||
setFile(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!file) {
|
||||
return toast.warn("Pilih file gambar terlebih dahulu");
|
||||
return toast.warn("Silakan pilih file gambar terlebih dahulu");
|
||||
}
|
||||
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file,
|
||||
name: file.name,
|
||||
})
|
||||
});
|
||||
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal mengupload file");
|
||||
return toast.error("Gagal mengunggah gambar, silakan coba lagi");
|
||||
}
|
||||
|
||||
stateProgramInovasi.create.form.imageId = uploaded.id;
|
||||
@@ -55,99 +68,116 @@ function CreateProgramInovasi() {
|
||||
router.push("/admin/landing-page/profile/program-inovasi")
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Program Inovasi
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create Program Inovasi</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{ 'image/*': [] }}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Program Inovasi
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Stack gap="xs" align="center">
|
||||
<Text size="md" fw={500}>
|
||||
Seret gambar atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Maksimal 5MB, format gambar wajib
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag gambar ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format gambar
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{/* Tampilkan preview kalau ada */}
|
||||
{previewImage && (
|
||||
<Box mt="sm">
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
{previewImage && (
|
||||
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<TextInput
|
||||
value={stateProgramInovasi.create.form.name || ''}
|
||||
onChange={(val) => {
|
||||
stateProgramInovasi.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Program Inovasi</Text>}
|
||||
placeholder='Masukkan nama program inovasi'
|
||||
/>
|
||||
<TextInput
|
||||
value={stateProgramInovasi.create.form.description || ''}
|
||||
onChange={(val) => {
|
||||
stateProgramInovasi.create.form.description = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
|
||||
placeholder='Masukkan deskripsi'
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
value={stateProgramInovasi.create.form.link || ''}
|
||||
onChange={(val) => {
|
||||
stateProgramInovasi.create.form.link = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Link</Text>}
|
||||
placeholder='Masukkan link'
|
||||
label="Nama Program Inovasi"
|
||||
placeholder="Masukkan nama program inovasi"
|
||||
value={stateProgramInovasi.create.form.name}
|
||||
onChange={(e) => (stateProgramInovasi.create.form.name = e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
||||
<CreateEditor
|
||||
value={stateProgramInovasi.create.form.description || ''}
|
||||
onChange={(htmlContent: string) => {
|
||||
stateProgramInovasi.create.form.description = htmlContent;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<TextInput
|
||||
label="Link Program Inovasi"
|
||||
placeholder="Masukkan link program inovasi (opsional)"
|
||||
value={stateProgramInovasi.create.form.link || ''}
|
||||
onChange={(e) => (stateProgramInovasi.create.form.link = e.target.value)}
|
||||
/>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import profileLandingPageState from '../../../_state/landing-page/profile';
|
||||
|
||||
function ProgramInovasi() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box px="md" py="lg">
|
||||
<HeaderSearch
|
||||
title='Program Inovasi'
|
||||
placeholder='pencarian'
|
||||
title="Program Inovasi"
|
||||
placeholder="Cari program inovasi..."
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
@@ -27,107 +27,116 @@ function ProgramInovasi() {
|
||||
}
|
||||
|
||||
function ListProgramInovasi({ search }: { search: string }) {
|
||||
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
|
||||
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateProgramInovasi.findMany;
|
||||
const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
const filteredData = data || []
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={550} />
|
||||
<Stack py={20}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Program Inovasi'
|
||||
href='/admin/landing-page/profile/program-inovasi/create'
|
||||
/>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Program</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Link</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Program Inovasi'
|
||||
href='/admin/landing-page/profile/program-inovasi/create'
|
||||
/>
|
||||
<Box style={{ overflowY: "auto" }}>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<Box py={15}>
|
||||
<Paper bg={colors['white-1']} withBorder p="lg" radius="md" shadow="sm">
|
||||
<Box mb="md" display="flex"
|
||||
style={{ justifyContent: 'space-between', alignItems: 'center' }}
|
||||
>
|
||||
<Title order={4}>Daftar Program Inovasi</Title>
|
||||
<Tooltip label="Tambah Program Inovasi" withArrow>
|
||||
<Button
|
||||
color="blue"
|
||||
leftSection={<IconPlus size={18} />}
|
||||
variant="light"
|
||||
radius="md"
|
||||
onClick={() => router.push('/admin/landing-page/profile/program-inovasi/create')}
|
||||
>
|
||||
Tambah Program
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover striped verticalSpacing="sm">
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Program</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Link</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd w={200}>{item.description}</TableTd>
|
||||
<TableTd>
|
||||
<Box w={250}>
|
||||
<a style={{ color: "black" }} href={item.link} target="_blank" rel="noopener noreferrer">
|
||||
<Text truncate fz={'sm'}>{item.link}</Text>
|
||||
</a>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
{filteredData.length === 0 ? (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text color="dimmed">Belum ada data program inovasi</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
) : (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fw={500}>{item.name}</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ maxWidth: 250 }}>
|
||||
<Text fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }}></Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ maxWidth: 250 }}>
|
||||
<Tooltip label="Buka tautan program" position="top" withArrow>
|
||||
<a
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: colors['blue-button'], textDecoration: 'underline' }}
|
||||
>
|
||||
<Text truncate fz="sm">{item.link}</Text>
|
||||
</a>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)
|
||||
}
|
||||
>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
{filteredData.length > 0 && (
|
||||
<Center mt="md">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
color="blue"
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Paper>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,10 +12,12 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title
|
||||
Title,
|
||||
Tooltip,
|
||||
Image
|
||||
} from "@mantine/core";
|
||||
import { Dropzone } from "@mantine/dropzone";
|
||||
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from "@tabler/icons-react";
|
||||
import { IconArrowBack, IconDeviceFloppy, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -97,82 +99,111 @@ function EditKolaborasiInovasi() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Edit SDGs Desa</Title>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit SDGs Desa
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar SDGs Desa
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{ 'image/*': [] }}
|
||||
radius="md"
|
||||
p="xl"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={180}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={48} color="red" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Stack gap="xs" align="center">
|
||||
<Text size="md" fw={500}>
|
||||
Seret gambar atau klik untuk memilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Maksimal 5MB, format gambar wajib
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewImage && (
|
||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview Gambar"
|
||||
radius="md"
|
||||
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<TextInput
|
||||
label="Nama SDGs Desa"
|
||||
placeholder="Masukkan nama SDGs Desa"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>}
|
||||
placeholder="masukkan nama"
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Jumlah"
|
||||
placeholder="Masukkan jumlah"
|
||||
value={formData.jumlah}
|
||||
onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Jumlah</Text>}
|
||||
placeholder="masukkan jumlah"
|
||||
required
|
||||
type="number"
|
||||
/>
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewImage ? (
|
||||
<iframe
|
||||
src={previewImage}
|
||||
width="100%"
|
||||
height="250px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button onClick={handleSubmit}>Simpan</Button>
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
leftSection={<IconDeviceFloppy size={20} />}
|
||||
loading={sdgsState.edit.loading}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -12,11 +11,11 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import sdgsDesa from '../../../_state/landing-page/sdgs-desa';
|
||||
|
||||
function DetailSDGSDesa() {
|
||||
const sdgsState = useProxy(sdgsDesa)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const sdgsState = useProxy(sdgsDesa);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
sdgsState.findUnique.load(params?.id as string)
|
||||
@@ -35,73 +34,104 @@ function DetailSDGSDesa() {
|
||||
if (!sdgsState.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={40} />
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = sdgsState.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail SDGS Desa</Text>
|
||||
{sdgsState.findUnique.data ? (
|
||||
<Paper key={sdgsState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Nama SDGS Desa</Text>
|
||||
<Text fz={"lg"}>{sdgsState.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Jumlah</Text>
|
||||
<Text fz={"lg"}>{sdgsState.findUnique.data?.jumlah}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={sdgsState.findUnique.data?.image?.link} alt="gambar" />
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Box py={10}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail SDGs Desa
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold" mb={4}>Nama SDGs Desa</Text>
|
||||
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold" mb={4}>Jumlah</Text>
|
||||
<Text fz="md" c="dimmed">{data.jumlah || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold" mb={4}>Gambar</Text>
|
||||
{data.image?.link ? (
|
||||
<Image
|
||||
src={data.image.link}
|
||||
alt={data.name || 'Gambar SDGs Desa'}
|
||||
w={200}
|
||||
h={200}
|
||||
radius="md"
|
||||
fit="contain"
|
||||
/>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group gap="sm" mt="md">
|
||||
<Tooltip label="Hapus SDGs Desa" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
if (sdgsState.findUnique.data) {
|
||||
setSelectedId(sdgsState.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
disabled={sdgsState.delete.loading || !sdgsState.findUnique.data}
|
||||
color={"red"}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={sdgsState.delete.loading}
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Edit SDGs Desa" withArrow position="top">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (sdgsState.findUnique.data) {
|
||||
router.push(`/admin/landing-page/SDGs-Desa/${sdgsState.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!sdgsState.findUnique.data}
|
||||
color={"green"}
|
||||
color="green"
|
||||
onClick={() => router.push(`/admin/landing-page/SDGs-Desa/${data.id}/edit`)}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus SDGS Desa ini?'
|
||||
text="Apakah Anda yakin ingin menghapus SDGs Desa ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { IconArrowBack, IconImageInPicture, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconDeviceFloppy, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -56,88 +56,138 @@ function CreateSDGsDesa() {
|
||||
router.push("/admin/landing-page/SDGs-Desa")
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah SDGs Desa
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create SDGs Desa</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz={"md"} fw={"bold"}>File Image</Text>
|
||||
<Box>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0]; // Ambil file pertama
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
||||
accept={{
|
||||
'application/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconImageInPicture size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar SDGs Desa
|
||||
</Text>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
const selectedFile = files[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
}}
|
||||
onReject={() => toast.error('File tidak valid.')}
|
||||
maxSize={5 * 1024 ** 2}
|
||||
accept={{
|
||||
'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.svg'],
|
||||
}}
|
||||
radius="md"
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload size={52} color={colors['blue-button']} stroke={1.5} />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk pilih file
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Text size="xl" inline>
|
||||
Drag file ke sini atau klik untuk memilih
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7} display="block">
|
||||
Maksimal 5MB (JPEG, JPG, PNG, GIF, WEBP, SVG)
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
{previewImage && (
|
||||
<Box mt="md">
|
||||
<Text fw={500} fz="sm" mb={4}>
|
||||
Pratinjau Gambar
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" inline mt={7}>
|
||||
Maksimal 5MB dan harus format image
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Dokumen</Text>
|
||||
{previewImage ? (
|
||||
<iframe
|
||||
src={previewImage}
|
||||
width="100%"
|
||||
height="250px"
|
||||
style={{ border: "1px solid #ccc", borderRadius: "8px" }}
|
||||
/>
|
||||
) : (
|
||||
<Text>Tidak ada image tersedia</Text>
|
||||
)}
|
||||
<Box
|
||||
style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
maxWidth: '300px'
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
style={{ width: '100%', height: 'auto' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<TextInput
|
||||
label={
|
||||
<Text fw="bold" fz="sm" mb={4}>
|
||||
Nama SDGs Desa
|
||||
</Text>
|
||||
}
|
||||
placeholder="Masukkan nama SDGs Desa"
|
||||
value={stateSDGSDesa.create.form.name}
|
||||
onChange={(val) => {
|
||||
stateSDGSDesa.create.form.name = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
|
||||
placeholder='Masukkan judul'
|
||||
required
|
||||
radius="md"
|
||||
/>
|
||||
<TextInput
|
||||
type='number'
|
||||
|
||||
<TextInput
|
||||
type="number"
|
||||
label={
|
||||
<Text fw="bold" fz="sm" mb={4}>
|
||||
Jumlah
|
||||
</Text>
|
||||
}
|
||||
placeholder="Masukkan jumlah"
|
||||
value={stateSDGSDesa.create.form.jumlah}
|
||||
onChange={(val) => {
|
||||
stateSDGSDesa.create.form.jumlah = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Jumlah</Text>}
|
||||
placeholder='Masukkan jumlah'
|
||||
required
|
||||
min={0}
|
||||
radius="md"
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconDeviceFloppy size={20} />}
|
||||
onClick={handleSubmit}
|
||||
loading={stateSDGSDesa.create.loading}
|
||||
color={colors['blue-button']}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import JudulList from '../../_com/judulList';
|
||||
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
|
||||
|
||||
|
||||
@@ -17,7 +16,7 @@ function SdgsDesa() {
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='SDGs Desa'
|
||||
placeholder='pencarian'
|
||||
placeholder='Cari SDGs Desa...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
@@ -57,20 +56,36 @@ function ListSdgsDesa({ search }: { search: string }) {
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Desa Anti Korupsi'
|
||||
href='/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create'
|
||||
/>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar SDGs Desa</Title>
|
||||
<Tooltip label="Tambah SDGs Desa" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color={colors['blue-button']}
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/SDGs-Desa/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama SDGs Desa</TableTh>
|
||||
<TableTh>Jumlah SDGs Desa</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
<TableTh style={{ width: '60%' }}>Nama SDGs Desa</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
|
||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
<TableTr>
|
||||
<TableTd colSpan={3} style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<Text c="dimmed">Tidak ada data SDGs Desa</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
@@ -80,54 +95,70 @@ function ListSdgsDesa({ search }: { search: string }) {
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<JudulList
|
||||
title='SDGs Desa'
|
||||
href='/admin/landing-page/SDGs-Desa/create'
|
||||
/>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama SDGs Desa</TableTh>
|
||||
<TableTh>Jumlah SDGs Desa</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>{item.jumlah}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/landing-page/SDGs-Desa/${item.id}`)}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar SDGs Desa</Title>
|
||||
<Tooltip label="Tambah SDGs Desa" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color={colors['blue-button']}
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/landing-page/SDGs-Desa/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '60%' }}>Nama SDGs Desa</TableTh>
|
||||
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
|
||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '60%' }}>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%' }}>
|
||||
<Text fz="sm" c="dimmed">
|
||||
{item.jumlah || '0'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '20%', textAlign: 'center' }}>
|
||||
<Tooltip label="Lihat Detail" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/admin/landing-page/SDGs-Desa/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImacCog size={18} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Center>
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
total={Math.max(1, totalPages)}
|
||||
withEdges
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
|
||||
@@ -42,18 +42,10 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10)
|
||||
}, [page])
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
|
||||
const filteredData = (data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.deskripsi.toLowerCase().includes(keyword) ||
|
||||
item.jumlah.toLowerCase().includes(keyword) ||
|
||||
item.icon.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
const filteredData = data || []
|
||||
|
||||
const iconMap: Record<string, React.FC<any>> = {
|
||||
ekowisata: IconLeaf,
|
||||
|
||||
@@ -1,63 +1,93 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { IconTrash, IconRecycle } from '@tabler/icons-react';
|
||||
|
||||
function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "List Pengelolaan Sampah Bank Sampah",
|
||||
value: "listpengelolaansampahbanksampah",
|
||||
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
|
||||
},
|
||||
{
|
||||
label: "Keterangan Bank Sampah Terdekat",
|
||||
value: "keteranganbanksampahterdekat",
|
||||
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat"
|
||||
},
|
||||
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
}
|
||||
setActiveTab(value)
|
||||
const tabs = [
|
||||
{
|
||||
label: "List Pengelolaan Sampah Bank Sampah",
|
||||
value: "listpengelolaansampahbanksampah",
|
||||
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah",
|
||||
icon: <IconTrash size={18} stroke={1.8} />,
|
||||
tooltip: "Kelola data pengelolaan sampah bank sampah",
|
||||
},
|
||||
{
|
||||
label: "Keterangan Bank Sampah Terdekat",
|
||||
value: "keteranganbanksampahterdekat",
|
||||
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat",
|
||||
icon: <IconRecycle size={18} stroke={1.8} />,
|
||||
tooltip: "Kelola data bank sampah terdekat",
|
||||
},
|
||||
];
|
||||
|
||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value);
|
||||
if (tab) {
|
||||
router.push(tab.href);
|
||||
}
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
}
|
||||
}, [pathname])
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname);
|
||||
if (match) {
|
||||
setActiveTab(match.value);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Layanan Online Desa</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Title order={3} mb="sm">Pengelolaan Sampah Bank Sampah</Title>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
variant="pills"
|
||||
radius="md"
|
||||
>
|
||||
<TabsList>
|
||||
{tabs.map((tab) => (
|
||||
<Tooltip
|
||||
key={tab.value}
|
||||
label={tab.tooltip}
|
||||
position="top"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 300 }}
|
||||
>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
height: 'auto',
|
||||
minHeight: 44,
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsPanel
|
||||
value={activeTab || ''}
|
||||
pt="lg"
|
||||
style={{
|
||||
minHeight: '60vh',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TabsPanel>
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabsPengelolaanSampahBankSampah;
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
'use client';
|
||||
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
@@ -64,63 +64,97 @@ function EditKeteranganBankSampahTerdekat() {
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (!formData.name.trim()) {
|
||||
return toast.error('Nama bank sampah harus diisi');
|
||||
}
|
||||
if (!formData.alamat.trim()) {
|
||||
return toast.error('Alamat harus diisi');
|
||||
}
|
||||
if (!formData.namaTempatMaps.trim()) {
|
||||
return toast.error('Nama tempat di Maps harus diisi');
|
||||
}
|
||||
if (!markerPosition) {
|
||||
return toast.error('Silakan pilih lokasi di peta');
|
||||
}
|
||||
|
||||
keteranganState.edit.form = {
|
||||
...keteranganState.edit.form,
|
||||
name: formData.name.trim(),
|
||||
alamat: formData.alamat.trim(),
|
||||
namaTempatMaps: formData.namaTempatMaps.trim(),
|
||||
lat: formData.lat,
|
||||
lng: formData.lng,
|
||||
}
|
||||
lat: markerPosition.lat,
|
||||
lng: markerPosition.lng,
|
||||
};
|
||||
|
||||
await keteranganState.edit.update();
|
||||
toast.success('Data bank sampah berhasil diperbarui');
|
||||
keteranganState.findUnique.data = null;
|
||||
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
|
||||
} catch (error) {
|
||||
console.error("Error updating pengelolaan sampah:", error);
|
||||
toast.error("Gagal memuat data pengelolaan sampah");
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data bank sampah');
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Bank Sampah Terdekat
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Edit Keterangan Bank Sampah Terdekat</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Bank Sampah"
|
||||
placeholder="Masukkan nama bank sampah"
|
||||
value={formData.name}
|
||||
onChange={(val) => setFormData({ ...formData, name: val.target.value })}
|
||||
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>}
|
||||
placeholder='Masukkan nama Bank Sampah Terdekat'
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Alamat"
|
||||
placeholder="Masukkan alamat lengkap"
|
||||
value={formData.alamat}
|
||||
onChange={(val) => setFormData({ ...formData, alamat: val.target.value })}
|
||||
label={<Text fw="bold" fz="sm">Alamat</Text>}
|
||||
placeholder='Masukkan alamat Bank Sampah'
|
||||
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Nama Tempat di Maps"
|
||||
placeholder="Masukkan nama tempat yang terdaftar di Google Maps"
|
||||
value={formData.namaTempatMaps}
|
||||
onChange={(val) => setFormData({ ...formData, namaTempatMaps: val.target.value })}
|
||||
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>}
|
||||
placeholder='Masukkan nama tempat maps Bank Sampah'
|
||||
onChange={(e) => setFormData({ ...formData, namaTempatMaps: e.target.value })}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text>
|
||||
<Box style={{ height: 300, width: '100%' }}>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Pilih Lokasi di Peta
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed" mb={4}>
|
||||
Klik pada peta untuk menandai lokasi
|
||||
</Text>
|
||||
<Box style={{ height: 300, width: '100%', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
<LeafletMapEdit
|
||||
key={markerPosition?.lat ?? 'default'}
|
||||
initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }}
|
||||
onChange={(pos) => {
|
||||
setMarkerPosition(pos);
|
||||
setFormData((prev) => ({
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
lat: pos.lat,
|
||||
lng: pos.lng,
|
||||
@@ -128,9 +162,26 @@ function EditKeteranganBankSampahTerdekat() {
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{markerPosition && (
|
||||
<Text fz="xs" mt={4} c="green">
|
||||
Lokasi dipilih: {markerPosition.lat.toFixed(6)}, {markerPosition.lng.toFixed(6)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Group>
|
||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -3,132 +3,148 @@
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { Anchor, Box, Button, Flex, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconArrowLeft, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), {
|
||||
ssr: false
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
function DetailKeteranganBankSampahTerdekat() {
|
||||
const router = useRouter();
|
||||
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const params = useParams()
|
||||
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const params = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
keteranganState.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
keteranganState.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
keteranganState.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat")
|
||||
keteranganState.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!keteranganState.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
<Stack p="md">
|
||||
<Skeleton h={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
<Box mb="md">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconArrowLeft size={20} />}
|
||||
onClick={() => router.back()}
|
||||
radius="xl"
|
||||
color="blue"
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail Keterangan Bank Sampah Terdekat</Text>
|
||||
<Paper
|
||||
w={{ base: "100%", md: "60%" }}
|
||||
p="xl"
|
||||
radius="lg"
|
||||
withBorder
|
||||
shadow="md"
|
||||
style={{ background: colors['white-1'] }}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<Title order={2} c="dark">
|
||||
Detail Bank Sampah Terdekat
|
||||
</Title>
|
||||
|
||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Paper p="lg" radius="md" withBorder >
|
||||
<Stack gap="md">
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Nama Bank Sampah Terdekat</Text>
|
||||
<Text fz={"lg"}>{keteranganState.findUnique.data?.name}</Text>
|
||||
<Text fz="sm" c="dimmed">Nama Bank Sampah</Text>
|
||||
<Text fz="lg" fw={600}>{keteranganState.findUnique.data?.name}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Alamat</Text>
|
||||
<Text fz={"lg"}>{keteranganState.findUnique.data?.alamat}</Text>
|
||||
<Text fz="sm" c="dimmed">Alamat</Text>
|
||||
<Text fz="lg">{keteranganState.findUnique.data?.alamat}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Nama Tempat Maps</Text>
|
||||
<Text fz={"lg"}>{keteranganState.findUnique.data?.namaTempatMaps}</Text>
|
||||
<Text fz="sm" c="dimmed">Nama Tempat di Maps</Text>
|
||||
<Text fz="lg">{keteranganState.findUnique.data?.namaTempatMaps}</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Peta Lokasi</Text>
|
||||
<Text fz="sm" c="dimmed" mb={6}>Peta Lokasi</Text>
|
||||
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
|
||||
<Box
|
||||
style={{
|
||||
height: "300px",
|
||||
}}
|
||||
>
|
||||
<Box style={{ height: "300px", borderRadius: "12px", overflow: "hidden" }}>
|
||||
<LeafletMap
|
||||
defaultCenter={{ lat: keteranganState.findUnique.data.lat, lng: keteranganState.findUnique.data.lng }}
|
||||
readOnly
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
|
||||
<Text c="dimmed" fz="sm">Belum ada koordinat</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
<Box>
|
||||
<Text fz={"lg"} fw={"bold"}>Link Petunjuk Arah</Text>
|
||||
<Text fz="sm" c="dimmed" mb={6}>Petunjuk Arah</Text>
|
||||
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
|
||||
<a
|
||||
<Anchor
|
||||
href={`https://www.google.com/maps/dir/?api=1&destination=${keteranganState.findUnique.data.lat},${keteranganState.findUnique.data.lng}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'black', textDecoration: 'underline' }}
|
||||
underline="always"
|
||||
c="blue"
|
||||
>
|
||||
Buka Petunjuk Arah di Google Maps
|
||||
</a>
|
||||
Buka di Google Maps
|
||||
</Anchor>
|
||||
) : (
|
||||
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
|
||||
<Text c="dimmed" fz="sm">Belum ada koordinat</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Flex gap={"xs"}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (keteranganState.findUnique.data) {
|
||||
setSelectedId(keteranganState.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={!keteranganState.findUnique.data}
|
||||
color="red"
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${keteranganState.findUnique.data?.id}/edit`)}
|
||||
color="green"
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Flex gap="sm" mt="md">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (keteranganState.findUnique.data) {
|
||||
setSelectedId(keteranganState.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={!keteranganState.findUnique.data}
|
||||
leftSection={<IconTrash size={18} />}
|
||||
color="red"
|
||||
radius="md"
|
||||
variant='light'
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${keteranganState.findUnique.data?.id}/edit`)
|
||||
}
|
||||
leftSection={<IconEdit size={18} />}
|
||||
color="green"
|
||||
radius="md"
|
||||
variant='light'
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
@@ -138,10 +154,9 @@ function DetailKeteranganBankSampahTerdekat() {
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text="Apakah anda yakin ingin menghapus keterangan bank sampah terdekat ini?"
|
||||
text="Apakah Anda yakin ingin menghapus data bank sampah ini?"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
@@ -28,58 +29,107 @@ function CreateKeteranganBankSampahTerdekat() {
|
||||
setMarkerPosition(null)
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
if (markerPosition) {
|
||||
keteranganState.create.form.lat = markerPosition.lat
|
||||
keteranganState.create.form.lng = markerPosition.lng
|
||||
try {
|
||||
if (!keteranganState.create.form.name) {
|
||||
return toast.error('Nama bank sampah harus diisi');
|
||||
}
|
||||
|
||||
if (markerPosition) {
|
||||
keteranganState.create.form.lat = markerPosition.lat;
|
||||
keteranganState.create.form.lng = markerPosition.lng;
|
||||
} else {
|
||||
return toast.error('Silakan pilih lokasi di peta');
|
||||
}
|
||||
|
||||
await keteranganState.create.create();
|
||||
toast.success('Data bank sampah berhasil ditambahkan');
|
||||
resetForm();
|
||||
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
|
||||
} catch (error) {
|
||||
console.error('Error creating bank sampah:', error);
|
||||
toast.error('Gagal menambahkan data bank sampah');
|
||||
}
|
||||
await keteranganState.create.create()
|
||||
resetForm()
|
||||
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat")
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Bank Sampah Terdekat
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create Keterangan Bank Sampah Terdekat</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Bank Sampah"
|
||||
placeholder="Masukkan nama bank sampah"
|
||||
value={keteranganState.create.form.name}
|
||||
onChange={(val) => keteranganState.create.form.name = val.target.value}
|
||||
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>}
|
||||
placeholder='Masukkan nama Bank Sampah Terdekat'
|
||||
onChange={(e) => (keteranganState.create.form.name = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Alamat"
|
||||
placeholder="Masukkan alamat lengkap"
|
||||
value={keteranganState.create.form.alamat}
|
||||
onChange={(val) => keteranganState.create.form.alamat = val.target.value}
|
||||
label={<Text fw="bold" fz="sm">Alamat</Text>}
|
||||
placeholder='Masukkan alamat Bank Sampah'
|
||||
onChange={(e) => (keteranganState.create.form.alamat = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Nama Tempat di Maps"
|
||||
placeholder="Masukkan nama tempat yang terdaftar di Google Maps"
|
||||
value={keteranganState.create.form.namaTempatMaps}
|
||||
onChange={(val) => keteranganState.create.form.namaTempatMaps = val.target.value}
|
||||
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>}
|
||||
placeholder='Masukkan nama tempat maps Bank Sampah'
|
||||
onChange={(e) => (keteranganState.create.form.namaTempatMaps = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text>
|
||||
<Box style={{ height: 300, width: '100%' }}>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Pilih Lokasi di Peta
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed" mb={4}>
|
||||
Klik pada peta untuk menandai lokasi
|
||||
</Text>
|
||||
<Box style={{ height: 300, width: '100%', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
<LeafletMap
|
||||
onSelect={(pos) => setMarkerPosition(pos)}
|
||||
defaultCenter={{ lat: -8.65, lng: 115.2 }}
|
||||
/>
|
||||
</Box>
|
||||
{markerPosition && (
|
||||
<Text fz="xs" mt={4} c="green">
|
||||
Lokasi dipilih: {markerPosition.lat.toFixed(6)}, {markerPosition.lng.toFixed(6)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Group>
|
||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
|
||||
|
||||
function KeteranganBankSampahTerdekat() {
|
||||
@@ -17,71 +15,124 @@ function KeteranganBankSampahTerdekat() {
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Keterangan Bank Sampah Terdekat'
|
||||
placeholder='pencarian'
|
||||
placeholder='Cari nama bank sampah...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListKeteranganBankSampahTerdekat search={search}/>
|
||||
<ListKeteranganBankSampahTerdekat search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListKeteranganBankSampahTerdekat({ search }: { search: string }) {
|
||||
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
|
||||
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
keteranganState.findMany.load()
|
||||
}, [])
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = keteranganState.findMany;
|
||||
|
||||
const filteredData = (keteranganState.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.alamat.toLowerCase().includes(keyword) ||
|
||||
item.namaTempatMaps.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
if (!keteranganState.findMany.data) {
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Keterangan Bank Sampah Terdekat'
|
||||
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create'
|
||||
/>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Bank Sampah Terdekat</TableTh>
|
||||
<TableTh>Alamat</TableTh>
|
||||
<TableTh>Nama Tempat Maps</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
<Paper bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Bank Sampah Terdekat</Title>
|
||||
<Tooltip label="Tambah Bank Sampah" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Bank Sampah</TableTh>
|
||||
<TableTh>Alamat</TableTh>
|
||||
<TableTh>Nama Tempat di Maps</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>{item.alamat}</TableTd>
|
||||
<TableTd>{item.namaTempatMaps}</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fw={500}>{item.name}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Box w={200}>
|
||||
<Text lineClamp={2} truncate="end" fz="sm">
|
||||
{item.alamat || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="sm">
|
||||
{item.namaTempatMaps || '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Tooltip label="Lihat Detail" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}
|
||||
>
|
||||
<IconDeviceImac size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={4} align="center" py="xl">
|
||||
<Text c="dimmed">Tidak ada data bank sampah terdekat</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
siblings={1}
|
||||
boundaries={1}
|
||||
withEdges
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
|
||||
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -65,47 +65,85 @@ function EditProgramKreatifDesa() {
|
||||
...stateSampah.update.form,
|
||||
name: formData.name.trim(),
|
||||
icon: formData.icon.trim(),
|
||||
}
|
||||
};
|
||||
|
||||
await stateSampah.update.submit();
|
||||
toast.success('Data pengelolaan sampah berhasil diperbarui!');
|
||||
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
|
||||
} catch (error) {
|
||||
console.error("Error updating pengelolaan sampah:", error);
|
||||
toast.error("Gagal memuat data pengelolaan sampah");
|
||||
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pengelolaan sampah");
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Pengelolaan Sampah Bank Sampah
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Edit List Pengelolaan Sampah Bank Sampah</Title>
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Pengelolaan Sampah"
|
||||
placeholder="Masukkan nama pengelolaan sampah"
|
||||
value={formData.name}
|
||||
label={<Text fz={"sm"} fw={"bold"}>Nama List Pengelolaan Sampah Bank Sampah</Text>}
|
||||
placeholder="masukkan nama list pengelolaan sampah bank sampah"
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
name: val.target.value
|
||||
})
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
name: value
|
||||
}));
|
||||
stateSampah.update.form.name = value;
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Ikon List Pengelolaan Sampah Bank Sampah</Text>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Pilih Ikon
|
||||
</Text>
|
||||
<SelectIconProgramEdit
|
||||
value={formData.icon as IconKey}
|
||||
onChange={(value) => {
|
||||
setFormData((prev) => ({ ...prev, icon: value }));
|
||||
setFormData(prev => ({ ...prev, icon: value }));
|
||||
stateSampah.update.form.icon = value;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
'use client';
|
||||
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
|
||||
import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
@@ -11,43 +11,83 @@ import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function CreatePengelolaanSampahBankSampah() {
|
||||
const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah)
|
||||
const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah);
|
||||
const router = useRouter();
|
||||
|
||||
const resetForm = () => {
|
||||
stateCreate.create.form = {
|
||||
name: "",
|
||||
icon: "",
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await stateCreate.create.create();
|
||||
resetForm();
|
||||
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah")
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
try {
|
||||
await stateCreate.create.create();
|
||||
resetForm();
|
||||
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
|
||||
} catch (error) {
|
||||
console.error('Error creating pengelolaan sampah:', error);
|
||||
}
|
||||
};
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Create List Pengelolaan Sampah Bank Sampah</Title>
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" position="bottom">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Pengelolaan Sampah Bank Sampah
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Nama Pengelolaan Sampah Bank Sampah</Text>}
|
||||
placeholder="masukkan nama pengelolaan sampah bank sampah"
|
||||
onChange={(val) => stateCreate.create.form.name = val.target.value}
|
||||
label="Nama Pengelolaan Sampah"
|
||||
placeholder="Masukkan nama pengelolaan sampah"
|
||||
value={stateCreate.create.form.name || ''}
|
||||
onChange={(e) => (stateCreate.create.form.name = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"}>Ikon Pengelolaan Sampah Bank Sampah</Text>
|
||||
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Pilih Ikon
|
||||
</Text>
|
||||
<SelectIconProgram
|
||||
onChange={(value) => (stateCreate.create.form.icon = value)}
|
||||
/>
|
||||
</Box>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconChartLine, IconClipboardTextFilled, IconEdit, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled, IconX } from '@tabler/icons-react';
|
||||
import { IconChartLine, IconClipboardTextFilled, IconEdit, IconLeaf, IconPlus, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
function PengelolaanSampahBankSampah() {
|
||||
const [search, setSearch] = useState("")
|
||||
const [search, setSearch] = useState("");
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='List Pengelolaan Sampah Bank Sampah'
|
||||
placeholder='pencarian'
|
||||
placeholder='Cari pengelolaan sampah...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
@@ -36,9 +34,18 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateList.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateList.findMany.load()
|
||||
}, [])
|
||||
load(page, 10, search)
|
||||
}, [page, search])
|
||||
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
@@ -48,15 +55,9 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
const filteredData = (stateList.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword)
|
||||
|| item.icon.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
const filteredData = data || []
|
||||
|
||||
const iconMap: Record<string, React.FC<any>> = {
|
||||
const iconMap: Record<string, React.FC<{ size: number; style?: React.CSSProperties }>> = {
|
||||
ekowisata: IconLeaf,
|
||||
kompetisi: IconTrophy,
|
||||
wisata: IconTent,
|
||||
@@ -68,7 +69,7 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
|
||||
trash: IconTrashFilled,
|
||||
};
|
||||
|
||||
if (!stateList.findMany.data) {
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
@@ -78,49 +79,107 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Pengelolaan Sampah Bank Sampah'
|
||||
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create'
|
||||
/>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Pengelolaan Sampah</TableTh>
|
||||
<TableTh>Icon</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd style={{ width: '10%' }}>
|
||||
{iconMap[item.icon] && (
|
||||
<Box title={item.icon}>
|
||||
{React.createElement(iconMap[item.icon], { size: 24 })}
|
||||
</Box>
|
||||
)}
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button color="green" onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${item.id}`)}>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button color="red" onClick={() => {
|
||||
setSelectedId(item.id)
|
||||
setModalHapus(true)
|
||||
}}>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<Paper bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Pengelolaan Sampah Bank Sampah</Title>
|
||||
<Tooltip label="Tambah Pengelolaan Sampah" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Pengelolaan Sampah</TableTh>
|
||||
<TableTh>Icon</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text lineClamp={1} truncate="end" fw={500}>{item.name}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
{iconMap[item.icon] ? (
|
||||
<Tooltip label={item.icon} withArrow>
|
||||
<Box>
|
||||
{React.createElement(iconMap[item.icon], {
|
||||
size: 24,
|
||||
style: { color: colors['blue-button'] }
|
||||
})}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Text c="dimmed" fz="sm">-</Text>
|
||||
)}
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Tooltip label="Edit" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${item.id}`)}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Tooltip label="Hapus" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrashFilled size={18} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={3} align="center" py="xl">
|
||||
<Text c="dimmed">Tidak ada data pengelolaan sampah</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
siblings={1}
|
||||
boundaries={1}
|
||||
withEdges
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
</Paper>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
|
||||
@@ -126,7 +126,9 @@ function ListProgramPenghijauan({ search }: { search: string }) {
|
||||
<TableTr key={item.id}>
|
||||
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
|
||||
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd>
|
||||
<TableTd style={{ width: '35%', wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: item.judul }}></TableTd>
|
||||
<TableTd style={{ width: '35%', wordWrap: 'break-word' }}>
|
||||
<Text lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.judul }}/>
|
||||
</TableTd>
|
||||
<TableTd style={{ width: '10%' }}>
|
||||
{iconMap[item.icon] && (
|
||||
<Box title={item.icon}>
|
||||
|
||||
@@ -1,63 +1,111 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IconSchool, IconStar } from '@tabler/icons-react';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "Beasiswa Pendaftar",
|
||||
value: "beasiswa-pendaftar",
|
||||
href: "/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar"
|
||||
href: "/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar",
|
||||
icon: <IconSchool size={18} stroke={1.8} />,
|
||||
tooltip: "Kelola data pendaftar beasiswa desa",
|
||||
},
|
||||
{
|
||||
label: "Keunggulan Program",
|
||||
value: "keunggulan-program",
|
||||
href: "/admin/pendidikan/beasiswa-desa/keunggulan-program"
|
||||
href: "/admin/pendidikan/beasiswa-desa/keunggulan-program",
|
||||
icon: <IconStar size={18} stroke={1.8} />,
|
||||
tooltip: "Lihat keunggulan dan detail program beasiswa",
|
||||
},
|
||||
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
|
||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
const tab = tabs.find(t => t.value === value);
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
router.push(tab.href);
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
const match = tabs.find(tab => tab.href === pathname);
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
setActiveTab(match.value);
|
||||
}
|
||||
}, [pathname])
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Beasiswa Desa</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
<Stack gap="lg">
|
||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||
Beasiswa Desa
|
||||
</Title>
|
||||
<Tabs
|
||||
color={colors['blue-button']}
|
||||
variant="pills"
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
key={i}
|
||||
value={tab.value}
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabs;
|
||||
export default LayoutTabs;
|
||||
|
||||
@@ -2,133 +2,150 @@
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconX } from '@tabler/icons-react';
|
||||
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
function DetailBeasiswaPendaftar() {
|
||||
const state = useProxy(beasiswaDesaState.beasiswaPendaftar)
|
||||
const state = useProxy(beasiswaDesaState.beasiswaPendaftar);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
state.findUnique.load(params?.id as string);
|
||||
}, []);
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
state.delete.delete(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar")
|
||||
state.delete.delete(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
router.push("/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!state.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = state.findUnique.data;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||
<Stack>
|
||||
<Flex justify="space-between" gap={"xs"}>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail Beasiswa Pendaftar</Text>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (state.findUnique.data) {
|
||||
setSelectedId(state.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={state.delete.loading || !state.findUnique.data}
|
||||
color={"red"}
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
{state.findUnique.data ? (
|
||||
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Nama Lengkap</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.namaLengkap}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>NIK</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.nik}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Tempat Lahir</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.tempatLahir}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Tanggal Lahir</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.tanggalLahir ? new Date(state.findUnique.data.tanggalLahir).toLocaleDateString() : '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Jenis Kelamin</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.jenisKelamin}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Kewarganegaraan</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.kewarganegaraan}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Agama</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.agama}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Alamat KTP</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.alamatKTP}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Alamat Domisili</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.alamatDomisili}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>No HP</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.noHp}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Email</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.email}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Status Pernikahan</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.statusPernikahan}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Ukuran Baju</Text>
|
||||
<Text fz={"lg"}>{state.findUnique.data?.ukuranBaju}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
<Box py={10}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||
mb={15}
|
||||
>
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: "100%", md: "70%" }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Beasiswa Pendaftar
|
||||
</Text>
|
||||
|
||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||
<Stack gap="sm">
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Nama Lengkap</Text>
|
||||
<Text fz="md" c="dimmed">{data.namaLengkap || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">NIK</Text>
|
||||
<Text fz="md" c="dimmed">{data.nik || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Tempat Lahir</Text>
|
||||
<Text fz="md" c="dimmed">{data.tempatLahir || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Tanggal Lahir</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data.tanggalLahir ? new Date(data.tanggalLahir).toLocaleDateString() : '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Jenis Kelamin</Text>
|
||||
<Text fz="md" c="dimmed">{data.jenisKelamin || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Kewarganegaraan</Text>
|
||||
<Text fz="md" c="dimmed">{data.kewarganegaraan || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Agama</Text>
|
||||
<Text fz="md" c="dimmed">{data.agama || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Alamat KTP</Text>
|
||||
<Text fz="md" c="dimmed">{data.alamatKTP || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Alamat Domisili</Text>
|
||||
<Text fz="md" c="dimmed">{data.alamatDomisili || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">No HP</Text>
|
||||
<Text fz="md" c="dimmed">{data.noHp || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Email</Text>
|
||||
<Text fz="md" c="dimmed">{data.email || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Status Pernikahan</Text>
|
||||
<Text fz="md" c="dimmed">{data.statusPernikahan || '-'}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold">Ukuran Baju</Text>
|
||||
<Text fz="md" c="dimmed">{data.ukuranBaju || '-'}</Text>
|
||||
</Box>
|
||||
|
||||
<Group gap="sm" mt="md">
|
||||
<Tooltip label="Hapus Pendaftar" withArrow position="top">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setSelectedId(data.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
variant="light"
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus beasiswa desa ini?'
|
||||
text="Apakah Anda yakin ingin menghapus data pendaftar ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,107 +1,149 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import beasiswaDesaState from '../../../_state/pendidikan/beasiswa-desa';
|
||||
|
||||
|
||||
function BeasiswaPendaftar() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Beasiswa Pendaftar'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListBeasiswaPendaftar search={search} />
|
||||
</Box>
|
||||
);
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Beasiswa Pendaftar'
|
||||
placeholder='Cari nama, alamat, atau data lain...'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListBeasiswaPendaftar search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListBeasiswaPendaftar({ search }: { search: string }) {
|
||||
const listDataState = useProxy(beasiswaDesaState.beasiswaPendaftar)
|
||||
const router = useRouter();
|
||||
const state = useProxy(beasiswaDesaState.beasiswaPendaftar);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
listDataState.findMany.load()
|
||||
}, [])
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
|
||||
const filteredData = (listDataState.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.namaLengkap.toLowerCase().includes(keyword) ||
|
||||
item.alamatKTP.toLowerCase().includes(keyword) ||
|
||||
(item.tanggalLahir ? new Date(item.tanggalLahir).toLocaleDateString() : '').toLowerCase().includes(keyword) ||
|
||||
item.jenisKelamin.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
useEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
if (!listDataState.findMany.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p="md">
|
||||
<Stack>
|
||||
<Title order={4}>List Beasiswa Pendaftar</Title>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>No</TableTh>
|
||||
<TableTh>Nama Lengkap</TableTh>
|
||||
<TableTh>Alamat</TableTh>
|
||||
<TableTh>Tanggal Lahir</TableTh>
|
||||
<TableTh>Jenis Kelamin</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>{item.namaLengkap}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>{item.alamatKTP}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>{item.tanggalLahir ? new Date(item.tanggalLahir).toLocaleDateString() : '-'}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>{item.jenisKelamin}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push(`/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar/${item.id}`)}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Stack py={10}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Beasiswa Pendaftar</Title>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>No</TableTh>
|
||||
<TableTh>Nama Lengkap</TableTh>
|
||||
<TableTh>Alamat</TableTh>
|
||||
<TableTh>Tanggal Lahir</TableTh>
|
||||
<TableTh>Jenis Kelamin</TableTh>
|
||||
<TableTh>Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item, index) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{index + 1}</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate fz="sm">{item.namaLengkap}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate fz="sm">{item.alamatKTP}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate fz="sm">
|
||||
{item.tanggalLahir ? new Date(item.tanggalLahir).toLocaleDateString() : '-'}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate fz="sm">{item.jenisKelamin}</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() =>
|
||||
router.push(`/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar/${item.id}`)
|
||||
}
|
||||
>
|
||||
<IconDeviceImacCog size={20} />
|
||||
<Text ml={5}>Detail</Text>
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={6}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed">Tidak ada data pendaftar yang cocok</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
)
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default BeasiswaPendaftar;
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function EditProgramKreatifDesa() {
|
||||
const state = useProxy(beasiswaDesaState.keunggulanProgram)
|
||||
const params = useParams()
|
||||
const router = useRouter();
|
||||
const [formData, setFormData] = useState({
|
||||
judul: '',
|
||||
deskripsi: '',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const loadProgramKreatif = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await state.update.load(id);
|
||||
if (data) {
|
||||
// ⬇️ FIX PENTING: tambahkan ini
|
||||
state.update.id = id;
|
||||
|
||||
state.update.form = {
|
||||
judul: data.judul,
|
||||
deskripsi: data.deskripsi,
|
||||
};
|
||||
|
||||
setFormData({
|
||||
judul: data.judul,
|
||||
deskripsi: data.deskripsi,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading pengelolaan sampah:", error);
|
||||
toast.error("Gagal memuat data pengelolaan sampah");
|
||||
}
|
||||
}
|
||||
|
||||
loadProgramKreatif();
|
||||
}, [params?.id]);
|
||||
|
||||
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
state.update.form = {
|
||||
...state.update.form,
|
||||
judul: formData.judul.trim(),
|
||||
deskripsi: formData.deskripsi.trim(),
|
||||
};
|
||||
|
||||
await state.update.update();
|
||||
toast.success('Data keunggulan program berhasil diperbarui!');
|
||||
router.push("/admin/pendidikan/beasiswa-desa/keunggulan-program");
|
||||
} catch (error) {
|
||||
console.error("Error updating keunggulan program:", error);
|
||||
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data keunggulan program");
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Keunggulan Program
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Judul"
|
||||
placeholder="Masukkan judul"
|
||||
value={formData.judul}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
judul: value
|
||||
}));
|
||||
state.update.form.judul = value;
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<EditEditor
|
||||
value={state.update.form.deskripsi}
|
||||
onChange={(htmlContent) => {
|
||||
state.update.form.deskripsi = htmlContent;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditProgramKreatifDesa;
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
|
||||
|
||||
function CreateKeunggulanProgram() {
|
||||
const stateCreate = useProxy(beasiswaDesaState.keunggulanProgram);
|
||||
const router = useRouter();
|
||||
|
||||
const resetForm = () => {
|
||||
stateCreate.create.form = {
|
||||
judul: "",
|
||||
deskripsi: "",
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await stateCreate.create.create();
|
||||
resetForm();
|
||||
router.push("/admin/pendidikan/beasiswa-desa/keunggulan-program");
|
||||
} catch (error) {
|
||||
console.error('Error creating keunggulan program:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => router.back()}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Tambah Keunggulan Program
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Judul"
|
||||
placeholder="Masukkan judul"
|
||||
value={stateCreate.create.form.judul || ''}
|
||||
onChange={(e) => (stateCreate.create.form.judul = e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Deskripsi
|
||||
</Text>
|
||||
<CreateEditor
|
||||
value={stateCreate.create.form.deskripsi}
|
||||
onChange={(htmlContent) => {
|
||||
stateCreate.create.form.deskripsi = htmlContent;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateKeunggulanProgram;
|
||||
@@ -1,71 +1,202 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrashFilled } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import beasiswaDesaState from '../../../_state/pendidikan/beasiswa-desa';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
|
||||
|
||||
function BeasiswaDesa() {
|
||||
function KeunggulanProgram() {
|
||||
const [search, setSearch] = useState("");
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Keunggulan Program'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
title="List Keunggulan Program"
|
||||
placeholder="Cari keunggulan program..."
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListBeasiswaDesa/>
|
||||
<ListKeunggulanProgram search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListBeasiswaDesa() {
|
||||
function ListKeunggulanProgram({ search }: { search: string }) {
|
||||
const stateList = useProxy(beasiswaDesaState.keunggulanProgram);
|
||||
const router = useRouter();
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const { data, page, totalPages, loading, load } = stateList.findMany;
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
stateList.delete.delete(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p="md">
|
||||
<Stack>
|
||||
<Title order={4}>List Beasiswa Desa</Title>
|
||||
<Box style={{overflowX: 'auto'}}>
|
||||
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
|
||||
<TableThead>
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Keunggulan Program</Title>
|
||||
<Tooltip label="Tambah Keunggulan Program" withArrow>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
'/admin/pendidikan/beasiswa-desa/keunggulan-program/create'
|
||||
)
|
||||
}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh style={{ width: '30%' }}>Nama Keunggulan Program</TableTh>
|
||||
<TableTh style={{ width: '35%' }}>Deskripsi</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Edit</TableTh>
|
||||
<TableTh style={{ width: '15%' }}>Delete</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>
|
||||
<Text fw={500} truncate="end" lineClamp={1}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text
|
||||
fw={500}
|
||||
truncate="end"
|
||||
lineClamp={1}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
/>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Tooltip label="Edit" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
color="green"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/pendidikan/beasiswa-desa/keunggulan-program/${item.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Tooltip label="Hapus" withArrow>
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrashFilled size={18} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTh>Nomor</TableTh>
|
||||
<TableTh>Nama Lengkap</TableTh>
|
||||
<TableTh>Nomor Telepon</TableTh>
|
||||
<TableTh>Email</TableTh>
|
||||
<TableTh>Detail</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
<TableTr>
|
||||
<TableTd>
|
||||
<Box w={100}>
|
||||
<Text truncate="end" fz={"sm"}>1</Text>
|
||||
</Box>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>Nama Lengkap</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>Nomor Telepon</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate="end" fz={"sm"}>Email</Text>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button onClick={() => router.push('/admin/pendidikan/beasiswa-desa/detail')}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
</Button>
|
||||
<TableTd colSpan={4}>
|
||||
<Center py={20}>
|
||||
<Text c="dimmed">
|
||||
Tidak ada data keunggulan program yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text="Apakah anda yakin ingin menghapus keunggulan program ini?"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default BeasiswaDesa;
|
||||
export default KeunggulanProgram;
|
||||
|
||||
@@ -1,68 +1,118 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IconSchool, IconCalendar, IconBuildingCommunity } from '@tabler/icons-react';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "Tujuan Program",
|
||||
value: "tujuan-program",
|
||||
href: "/admin/pendidikan/bimbingan-belajar-desa/tujuan-program"
|
||||
href: "/admin/pendidikan/bimbingan-belajar-desa/tujuan-program",
|
||||
icon: <IconSchool size={18} stroke={1.8} />,
|
||||
tooltip: "Lihat tujuan utama program bimbingan belajar",
|
||||
},
|
||||
{
|
||||
label: "Lokasi dan Jadwal",
|
||||
value: "lokasi-dan-jadwal",
|
||||
href: "/admin/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal"
|
||||
href: "/admin/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal",
|
||||
icon: <IconCalendar size={18} stroke={1.8} />,
|
||||
tooltip: "Atur lokasi pelaksanaan dan jadwal bimbingan",
|
||||
},
|
||||
{
|
||||
label: "Fasilitas yang disediakan",
|
||||
label: "Fasilitas yang Disediakan",
|
||||
value: "fasilitas-yang-disediakan",
|
||||
href: "/admin/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan"
|
||||
href: "/admin/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan",
|
||||
icon: <IconBuildingCommunity size={18} stroke={1.8} />,
|
||||
tooltip: "Kelola fasilitas yang tersedia untuk peserta",
|
||||
},
|
||||
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
|
||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
const tab = tabs.find(t => t.value === value);
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
router.push(tab.href);
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
const match = tabs.find(tab => tab.href === pathname);
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
setActiveTab(match.value);
|
||||
}
|
||||
}, [pathname])
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Bimbingan Belajar Desa</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
<Stack gap="lg">
|
||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||
Bimbingan Belajar Desa
|
||||
</Title>
|
||||
<Tabs
|
||||
color={colors['blue-button']}
|
||||
variant="pills"
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
radius="lg"
|
||||
keepMounted={false}
|
||||
>
|
||||
<TabsList
|
||||
p="sm"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, i) => (
|
||||
<Tooltip
|
||||
key={i}
|
||||
label={tab.tooltip}
|
||||
position="bottom"
|
||||
withArrow
|
||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||
>
|
||||
<TabsTab
|
||||
value={tab.value}
|
||||
leftSection={tab.icon}
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.9rem",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTab>
|
||||
</Tooltip>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
|
||||
{tabs.map((tab, i) => (
|
||||
<TabsPanel
|
||||
key={i}
|
||||
value={tab.value}
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||
borderRadius: "1rem",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabs;
|
||||
export default LayoutTabs;
|
||||
|
||||
@@ -1,86 +1,167 @@
|
||||
'use client'
|
||||
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const BimbinganBelajarDesaTextEditor = dynamic(() => import('../../_lib/bimbinganBelajarDesaTextEditor').then(mod => mod.BimbinganBelajarDesaTextEditor), {
|
||||
ssr: false,
|
||||
});
|
||||
const BimbinganBelajarDesaTextEditor = dynamic(
|
||||
() =>
|
||||
import('../../_lib/bimbinganBelajarDesaTextEditor').then(
|
||||
(mod) => mod.BimbinganBelajarDesaTextEditor
|
||||
),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
function EditFasilitasYangDisediakan() {
|
||||
const router = useRouter();
|
||||
const editState = useProxy(stateBimbinganBelajarDesa.fasilitasYangDisediakanState);
|
||||
|
||||
function EditTujuanProgram() {
|
||||
const router = useRouter()
|
||||
const editState = useProxy(stateBimbinganBelajarDesa.fasilitasYangDisediakanState)
|
||||
const [judul, setJudul] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Load data pertama kali
|
||||
useShallowEffect(() => {
|
||||
if (!editState.findById.data) {
|
||||
editState.findById.initialize(); // biar masuk ke `findFirst` route kamu
|
||||
editState.findById.initialize();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Sinkronkan state dengan data yang sudah di-load
|
||||
useEffect(() => {
|
||||
if (editState.findById.data) {
|
||||
setJudul(editState.findById.data.judul ?? '')
|
||||
setContent(editState.findById.data.deskripsi ?? '')
|
||||
setJudul(editState.findById.data.judul ?? '');
|
||||
setContent(editState.findById.data.deskripsi ?? '');
|
||||
}
|
||||
}, [editState.findById.data])
|
||||
}, [editState.findById.data]);
|
||||
|
||||
const submit = () => {
|
||||
if (editState.findById.data) {
|
||||
editState.findById.data.judul = judul;
|
||||
editState.findById.data.deskripsi = content;
|
||||
editState.update.save(editState.findById.data)
|
||||
const handleSubmit = async () => {
|
||||
if (!judul.trim()) {
|
||||
toast.error('Judul wajib diisi');
|
||||
return;
|
||||
}
|
||||
router.push('/admin/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan')
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (editState.findById.data) {
|
||||
editState.findById.data.judul = judul;
|
||||
editState.findById.data.deskripsi = content;
|
||||
await editState.update.save(editState.findById.data);
|
||||
|
||||
toast.success('Berhasil menyimpan perubahan');
|
||||
router.push('/admin/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving:', err);
|
||||
toast.error('Gagal menyimpan data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => router.back();
|
||||
|
||||
// Loading state
|
||||
if (editState.findById.loading) {
|
||||
return (
|
||||
<Box>
|
||||
<Center h={400}>
|
||||
<Text>Memuat data fasilitas yang disediakan...</Text>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap={'xs'}>
|
||||
<Box>
|
||||
<Button
|
||||
variant={'subtle'}
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box>
|
||||
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
|
||||
<Stack gap={'xs'}>
|
||||
<Title order={3}>Edit Fasilitas Yang Disediakan</Title>
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
||||
value={judul}
|
||||
onChange={(e) => setJudul(e.target.value)}
|
||||
/>
|
||||
<Text fw={"bold"}>Deskripsi</Text>
|
||||
<Stack gap="xs">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={handleBack}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Fasilitas Yang Disediakan
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="md"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Formulir Edit</Title>
|
||||
|
||||
{/* Judul */}
|
||||
<TextInput
|
||||
label={<Text fw="bold">Judul</Text>}
|
||||
placeholder="Masukkan judul fasilitas"
|
||||
value={judul}
|
||||
onChange={(e) => setJudul(e.currentTarget.value)}
|
||||
error={!judul && 'Judul wajib diisi'}
|
||||
/>
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold" mb="xs">Deskripsi</Text>
|
||||
<BimbinganBelajarDesaTextEditor
|
||||
showSubmit={false}
|
||||
onChange={setContent}
|
||||
initialContent={content}
|
||||
/>
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={submit}
|
||||
loading={editState.update.loading}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Tombol Aksi */}
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
loading={isSubmitting || editState.update.loading}
|
||||
disabled={!judul}
|
||||
>
|
||||
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleBack}
|
||||
disabled={isSubmitting || editState.update.loading}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditTujuanProgram;
|
||||
export default EditFasilitasYangDisediakan;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { Box, Button, Divider, Grid, GridCol, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -8,47 +8,86 @@ import { useProxy } from 'valtio/utils';
|
||||
import stateBimbinganBelajarDesa from '../../../_state/pendidikan/bimbingan-belajar-desa';
|
||||
|
||||
function Page() {
|
||||
const router = useRouter()
|
||||
const listPreview = useProxy(stateBimbinganBelajarDesa.fasilitasYangDisediakanState)
|
||||
const router = useRouter();
|
||||
const listPreview = useProxy(stateBimbinganBelajarDesa.fasilitasYangDisediakanState);
|
||||
|
||||
useShallowEffect(() => {
|
||||
listPreview.findById.load('edit')
|
||||
}, [])
|
||||
listPreview.findById.load('edit');
|
||||
}, []);
|
||||
|
||||
if (!listPreview.findById.data) {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton radius={10} h={800} />
|
||||
<Stack align="center" justify="center" py="xl">
|
||||
<Skeleton radius="md" height={400} w="90%" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = listPreview.findById.data;
|
||||
|
||||
return (
|
||||
<Paper bg={colors['white-1']} p={'md'} radius={10}>
|
||||
<Stack gap={"22"}>
|
||||
<Grid>
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={2}>Preview Fasilitas Yang Disediakan</Title>
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
Pratinjau Fasilitas Yang Disediakan
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit')}>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
<Tooltip label="Edit Fasilitas" withArrow>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
'/admin/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan/edit'
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Box>
|
||||
<Stack gap={'lg'}>
|
||||
<Paper p={"xl"} bg={colors['BG-trans']}>
|
||||
<Box px={{ base: 0, md: 30 }}>
|
||||
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.judul }} />
|
||||
</Box>
|
||||
<Box px={{ base: 0, md: 30 }}>
|
||||
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.deskripsi }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Konten Preview */}
|
||||
<Paper
|
||||
p="xl"
|
||||
bg={colors['white-1']}
|
||||
withBorder
|
||||
radius="md"
|
||||
shadow="xs"
|
||||
>
|
||||
<Box px={{ base: 'sm', md: 50 }}>
|
||||
<Stack gap="lg">
|
||||
{/* Judul */}
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: '1.3rem', md: '1.8rem' }}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
dangerouslySetInnerHTML={{ __html: data.judul }}
|
||||
/>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Text
|
||||
fz={{ base: '1rem', md: '1.2rem' }}
|
||||
lh={1.7}
|
||||
ta="justify"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -1,86 +1,169 @@
|
||||
'use client'
|
||||
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const BimbinganBelajarDesaTextEditor = dynamic(() => import('../../_lib/bimbinganBelajarDesaTextEditor').then(mod => mod.BimbinganBelajarDesaTextEditor), {
|
||||
ssr: false,
|
||||
});
|
||||
const BimbinganBelajarDesaTextEditor = dynamic(
|
||||
() =>
|
||||
import('../../_lib/bimbinganBelajarDesaTextEditor').then(
|
||||
(mod) => mod.BimbinganBelajarDesaTextEditor
|
||||
),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
function EditLokasiDanJadwal() {
|
||||
const router = useRouter();
|
||||
const editState = useProxy(stateBimbinganBelajarDesa.lokasiDanJadwalState);
|
||||
|
||||
function EditTujuanProgram() {
|
||||
const router = useRouter()
|
||||
const editState = useProxy(stateBimbinganBelajarDesa.lokasiDanJadwalState)
|
||||
const [judul, setJudul] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// load data sekali
|
||||
useShallowEffect(() => {
|
||||
if (!editState.findById.data) {
|
||||
editState.findById.initialize(); // biar masuk ke `findFirst` route kamu
|
||||
editState.findById.initialize();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// isi state ketika data loaded
|
||||
useEffect(() => {
|
||||
if (editState.findById.data) {
|
||||
setJudul(editState.findById.data.judul ?? '')
|
||||
setContent(editState.findById.data.deskripsi ?? '')
|
||||
setJudul(editState.findById.data.judul ?? '');
|
||||
setContent(editState.findById.data.deskripsi ?? '');
|
||||
}
|
||||
}, [editState.findById.data])
|
||||
}, [editState.findById.data]);
|
||||
|
||||
const submit = () => {
|
||||
if (editState.findById.data) {
|
||||
editState.findById.data.judul = judul;
|
||||
editState.findById.data.deskripsi = content;
|
||||
editState.update.save(editState.findById.data)
|
||||
const handleSubmit = async () => {
|
||||
if (!judul.trim()) {
|
||||
toast.error('Judul wajib diisi');
|
||||
return;
|
||||
}
|
||||
router.push('/admin/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal')
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (editState.findById.data) {
|
||||
editState.findById.data.judul = judul;
|
||||
editState.findById.data.deskripsi = content;
|
||||
await editState.update.save(editState.findById.data);
|
||||
|
||||
toast.success('Berhasil menyimpan perubahan');
|
||||
router.push('/admin/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving:', err);
|
||||
toast.error('Gagal menyimpan data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => router.back();
|
||||
|
||||
// loading state
|
||||
if (editState.findById.loading) {
|
||||
return (
|
||||
<Box>
|
||||
<Center h={400}>
|
||||
<Text>Memuat data lokasi & jadwal...</Text>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap={'xs'}>
|
||||
<Box>
|
||||
<Button
|
||||
variant={'subtle'}
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box>
|
||||
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
|
||||
<Stack gap={'xs'}>
|
||||
<Title order={3}>Edit Lokasi Dan Jadwal</Title>
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
||||
value={judul}
|
||||
onChange={(e) => setJudul(e.target.value)}
|
||||
/>
|
||||
<Text fw={"bold"}>Deskripsi</Text>
|
||||
<Stack gap="xs">
|
||||
{/* Header dengan tombol kembali */}
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={handleBack}
|
||||
p="xs"
|
||||
radius="md"
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Lokasi & Jadwal
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
{/* Form Card */}
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="md"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Form Lokasi & Jadwal</Title>
|
||||
|
||||
{/* Judul Field */}
|
||||
<TextInput
|
||||
label={<Text fw="bold">Judul</Text>}
|
||||
placeholder="Masukkan judul lokasi/jadwal"
|
||||
value={judul}
|
||||
onChange={(e) => setJudul(e.currentTarget.value)}
|
||||
error={!judul && 'Judul wajib diisi'}
|
||||
/>
|
||||
|
||||
{/* Deskripsi Field */}
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold" mb="xs">Deskripsi</Text>
|
||||
<BimbinganBelajarDesaTextEditor
|
||||
showSubmit={false}
|
||||
onChange={setContent}
|
||||
initialContent={content}
|
||||
/>
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={submit}
|
||||
loading={editState.update.loading}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Action Button */}
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
loading={isSubmitting || editState.update.loading}
|
||||
disabled={!judul}
|
||||
>
|
||||
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleBack}
|
||||
disabled={isSubmitting || editState.update.loading}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditTujuanProgram;
|
||||
export default EditLokasiDanJadwal;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { Box, Button, Divider, Grid, GridCol, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -8,47 +8,86 @@ import { useProxy } from 'valtio/utils';
|
||||
import stateBimbinganBelajarDesa from '../../../_state/pendidikan/bimbingan-belajar-desa';
|
||||
|
||||
function Page() {
|
||||
const router = useRouter()
|
||||
const listPreview = useProxy(stateBimbinganBelajarDesa.lokasiDanJadwalState)
|
||||
const router = useRouter();
|
||||
const listPreview = useProxy(stateBimbinganBelajarDesa.lokasiDanJadwalState);
|
||||
|
||||
useShallowEffect(() => {
|
||||
listPreview.findById.load('edit')
|
||||
}, [])
|
||||
listPreview.findById.load('edit');
|
||||
}, []);
|
||||
|
||||
if (!listPreview.findById.data) {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton radius={10} h={800} />
|
||||
<Stack align="center" justify="center" py="xl">
|
||||
<Skeleton radius="md" height={400} w="90%" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = listPreview.findById.data;
|
||||
|
||||
return (
|
||||
<Paper bg={colors['white-1']} p={'md'} radius={10}>
|
||||
<Stack gap={"22"}>
|
||||
<Grid>
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={2}>Preview Lokasi Dan Jadwal</Title>
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
Pratinjau Lokasi & Jadwal
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit')}>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
<Tooltip label="Edit Lokasi & Jadwal" withArrow>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
'/admin/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal/edit'
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Box>
|
||||
<Stack gap={'lg'}>
|
||||
<Paper p={"xl"} bg={colors['BG-trans']}>
|
||||
<Box px={{ base: 0, md: 30 }}>
|
||||
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.judul }} />
|
||||
</Box>
|
||||
<Box px={{ base: 0, md: 30 }}>
|
||||
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.deskripsi }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Konten Preview */}
|
||||
<Paper
|
||||
p="xl"
|
||||
bg={colors['white-1']}
|
||||
withBorder
|
||||
radius="md"
|
||||
shadow="xs"
|
||||
>
|
||||
<Box px={{ base: 'sm', md: 50 }}>
|
||||
<Stack gap="lg">
|
||||
{/* Judul */}
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: '1.3rem', md: '1.8rem' }}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
dangerouslySetInnerHTML={{ __html: data.judul }}
|
||||
/>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Text
|
||||
fz={{ base: '1rem', md: '1.2rem' }}
|
||||
lh={1.7}
|
||||
ta="justify"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -1,83 +1,158 @@
|
||||
'use client'
|
||||
import stateBimbinganBelajarDesa from '@/app/admin/(dashboard)/_state/pendidikan/bimbingan-belajar-desa';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const BimbinganBelajarDesaTextEditor = dynamic(() => import('../../_lib/bimbinganBelajarDesaTextEditor').then(mod => mod.BimbinganBelajarDesaTextEditor), {
|
||||
ssr: false,
|
||||
});
|
||||
const BimbinganBelajarDesaTextEditor = dynamic(
|
||||
() =>
|
||||
import('../../_lib/bimbinganBelajarDesaTextEditor').then(
|
||||
(mod) => mod.BimbinganBelajarDesaTextEditor
|
||||
),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
function EditTujuanProgram() {
|
||||
const router = useRouter()
|
||||
const editState = useProxy(stateBimbinganBelajarDesa.stateTujuanProgram)
|
||||
const router = useRouter();
|
||||
const editState = useProxy(stateBimbinganBelajarDesa.stateTujuanProgram);
|
||||
|
||||
const [judul, setJudul] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// load data once
|
||||
useShallowEffect(() => {
|
||||
if (!editState.findById.data) {
|
||||
editState.findById.initialize(); // biar masuk ke `findFirst` route kamu
|
||||
editState.findById.initialize();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (editState.findById.data) {
|
||||
setJudul(editState.findById.data.judul ?? '')
|
||||
setContent(editState.findById.data.deskripsi ?? '')
|
||||
setJudul(editState.findById.data.judul ?? '');
|
||||
setContent(editState.findById.data.deskripsi ?? '');
|
||||
}
|
||||
}, [editState.findById.data])
|
||||
}, [editState.findById.data]);
|
||||
|
||||
const submit = () => {
|
||||
if (editState.findById.data) {
|
||||
editState.findById.data.judul = judul;
|
||||
editState.findById.data.deskripsi = content;
|
||||
editState.update.save(editState.findById.data)
|
||||
const handleSubmit = async () => {
|
||||
if (!judul.trim()) {
|
||||
toast.error('Judul wajib diisi');
|
||||
return;
|
||||
}
|
||||
router.push('/admin/pendidikan/bimbingan-belajar-desa/tujuan-program')
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (editState.findById.data) {
|
||||
editState.findById.data.judul = judul;
|
||||
editState.findById.data.deskripsi = content;
|
||||
await editState.update.save(editState.findById.data);
|
||||
|
||||
toast.success('Berhasil menyimpan perubahan');
|
||||
router.push('/admin/pendidikan/bimbingan-belajar-desa/tujuan-program');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving:', err);
|
||||
toast.error('Gagal menyimpan data');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => router.back();
|
||||
|
||||
// loading state
|
||||
if (editState.findById.loading) {
|
||||
return (
|
||||
<Box>
|
||||
<Center h={400}>
|
||||
<Text>Memuat data tujuan program...</Text>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap={'xs'}>
|
||||
<Box>
|
||||
<Button
|
||||
variant={'subtle'}
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box>
|
||||
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
|
||||
<Stack gap={'xs'}>
|
||||
<Title order={3}>Edit Tujuan Program</Title>
|
||||
<TextInput
|
||||
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
|
||||
value={judul}
|
||||
onChange={(e) => setJudul(e.target.value)}
|
||||
/>
|
||||
<Text fw={"bold"}>Deskripsi</Text>
|
||||
<Stack gap="xs">
|
||||
<Group mb="md">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Tujuan Program
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
w={{ base: '100%', md: '50%' }}
|
||||
bg={colors['white-1']}
|
||||
p="md"
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
style={{ border: '1px solid #e0e0e0' }}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Form Edit Tujuan Program</Title>
|
||||
|
||||
{/* Judul Field */}
|
||||
<TextInput
|
||||
label={<Text fw="bold">Judul</Text>}
|
||||
placeholder="Masukkan judul program"
|
||||
value={judul}
|
||||
onChange={(e) => setJudul(e.currentTarget.value)}
|
||||
error={!judul && 'Judul wajib diisi'}
|
||||
/>
|
||||
|
||||
{/* Deskripsi Field */}
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold" mb="xs">Deskripsi</Text>
|
||||
<BimbinganBelajarDesaTextEditor
|
||||
showSubmit={false}
|
||||
onChange={setContent}
|
||||
initialContent={content}
|
||||
/>
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={submit}
|
||||
loading={editState.update.loading}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Submit & Cancel */}
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
loading={isSubmitting || editState.update.loading}
|
||||
disabled={!judul}
|
||||
>
|
||||
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleBack}
|
||||
disabled={isSubmitting || editState.update.loading}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
Grid,
|
||||
GridCol,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -8,47 +20,86 @@ import { useProxy } from 'valtio/utils';
|
||||
import stateBimbinganBelajarDesa from '../../../_state/pendidikan/bimbingan-belajar-desa';
|
||||
|
||||
function Page() {
|
||||
const router = useRouter()
|
||||
const listPreview = useProxy(stateBimbinganBelajarDesa.stateTujuanProgram)
|
||||
const router = useRouter();
|
||||
const listPreview = useProxy(stateBimbinganBelajarDesa.stateTujuanProgram);
|
||||
|
||||
useShallowEffect(() => {
|
||||
listPreview.findById.load('edit')
|
||||
}, [])
|
||||
listPreview.findById.load('edit');
|
||||
}, []);
|
||||
|
||||
if (!listPreview.findById.data) {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton radius={10} h={800} />
|
||||
<Stack align="center" justify="center" py="xl">
|
||||
<Skeleton radius="md" height={400} w="90%" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = listPreview.findById.data;
|
||||
|
||||
return (
|
||||
<Paper bg={colors['white-1']} p={'md'} radius={10}>
|
||||
<Stack gap={"22"}>
|
||||
<Grid>
|
||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Grid align="center">
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={2}>Preview Tujuan Program</Title>
|
||||
<Title order={3} c={colors['blue-button']}>
|
||||
Pratinjau Tujuan Program
|
||||
</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/pendidikan/bimbingan-belajar-desa/tujuan-program/edit')}>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
<Tooltip label="Edit Tujuan Program" withArrow>
|
||||
<Button
|
||||
c="green"
|
||||
variant="light"
|
||||
leftSection={<IconEdit size={18} stroke={2} />}
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
'/admin/pendidikan/bimbingan-belajar-desa/tujuan-program/edit'
|
||||
)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Box>
|
||||
<Stack gap={'lg'}>
|
||||
<Paper p={"xl"} bg={colors['BG-trans']}>
|
||||
<Box px={{ base: 0, md: 30 }}>
|
||||
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.judul }} />
|
||||
</Box>
|
||||
<Box px={{ base: 0, md: 30 }}>
|
||||
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listPreview.findById.data.deskripsi }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Konten Preview */}
|
||||
<Paper
|
||||
p="xl"
|
||||
bg={colors['white-1']}
|
||||
withBorder
|
||||
radius="md"
|
||||
shadow="xs"
|
||||
>
|
||||
<Box px={{ base: 'sm', md: 50 }}>
|
||||
<Stack gap="lg">
|
||||
{/* Judul */}
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: '1.3rem', md: '1.8rem' }}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
dangerouslySetInnerHTML={{ __html: data.judul }}
|
||||
/>
|
||||
<Divider my="md" color={colors['blue-button']} />
|
||||
|
||||
{/* Deskripsi */}
|
||||
<Text
|
||||
fz={{ base: '1rem', md: '1.2rem' }}
|
||||
lh={1.7}
|
||||
ta="justify"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi }}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -2,79 +2,94 @@
|
||||
'use client'
|
||||
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import dataPendidikan from '../../../_state/pendidikan/data-pendidikan';
|
||||
|
||||
function EditDataPendidikan() {
|
||||
const router = useRouter()
|
||||
const params = useParams() as { id: string }
|
||||
const stateDPM = useProxy(dataPendidikan)
|
||||
export default function EditDataPendidikan() {
|
||||
const router = useRouter();
|
||||
const params = useParams() as { id: string };
|
||||
const stateDPM = useProxy(dataPendidikan);
|
||||
const id = params.id;
|
||||
|
||||
const id = params.id
|
||||
|
||||
// Load data saat komponen mount
|
||||
// Load data saat mount
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
stateDPM.findUnique.load(id).then(() => {
|
||||
const data = stateDPM.findUnique.data
|
||||
const data = stateDPM.findUnique.data;
|
||||
if (data) {
|
||||
stateDPM.update.form = {
|
||||
name: data.name || "",
|
||||
jumlah: data.jumlah || "",
|
||||
}
|
||||
name: data.name || '',
|
||||
jumlah: data.jumlah || '',
|
||||
};
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [id])
|
||||
}, [id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Set the ID before submitting
|
||||
stateDPM.update.id = id;
|
||||
await stateDPM.update.submit();
|
||||
router.push('/admin/pendidikan/data-pendidikan')
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack size={20} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
|
||||
<Stack>
|
||||
<Title order={3}>Edit Data Pendidikan</Title>
|
||||
<TextInput
|
||||
label="Nama"
|
||||
placeholder="masukkan nama"
|
||||
value={stateDPM.update.form.name}
|
||||
onChange={(val) => {
|
||||
stateDPM.update.form.name = val.currentTarget.value;
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Jumlah"
|
||||
type="number"
|
||||
placeholder="masukkan jumlah"
|
||||
value={stateDPM.update.form.jumlah}
|
||||
onChange={(val) => {
|
||||
stateDPM.update.form.jumlah = val.currentTarget.value;
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
mt={10}
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Simpan
|
||||
router.push('/admin/pendidikan/data-pendidikan');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'sm', md: 'xl' }} py="md">
|
||||
<Group mb="md" gap="sm">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" position="bottom" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} radius="md">
|
||||
<IconArrowBack size={20} stroke={2} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={3} c="black">Edit Data Pendidikan</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="xl"
|
||||
bg="white"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Pendidikan"
|
||||
placeholder="Contoh: SD, SMP, SMA"
|
||||
value={stateDPM.update.form.name}
|
||||
onChange={(e) => (stateDPM.update.form.name = e.currentTarget.value)}
|
||||
radius="md"
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
label="Jumlah Peserta"
|
||||
type="number"
|
||||
placeholder="Masukkan jumlah peserta"
|
||||
value={stateDPM.update.form.jumlah}
|
||||
onChange={(e) => (stateDPM.update.form.jumlah = e.currentTarget.value)}
|
||||
radius="md"
|
||||
required
|
||||
/>
|
||||
<Group justify="right" mt="sm">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, ${colors['blue-button-trans']})`,
|
||||
color: 'white',
|
||||
boxShadow: '0 0 10px rgba(0,123,255,0.5)',
|
||||
'&:hover': { opacity: 0.9 },
|
||||
}}
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default EditDataPendidikan;
|
||||
|
||||
@@ -1,81 +1,93 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import dataPendidikan from '../../../_state/pendidikan/data-pendidikan';
|
||||
|
||||
function CreateDataPendidikan() {
|
||||
export default function CreateDataPendidikan() {
|
||||
const stateDPM = useProxy(dataPendidikan);
|
||||
const [chartData, setChartData] = useState<any[]>([]);
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
const resetForm = () => {
|
||||
stateDPM.create.form = {
|
||||
name: "",
|
||||
jumlah: "",
|
||||
}
|
||||
}
|
||||
stateDPM.create.form = { name: '', jumlah: '' };
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const id = await stateDPM.create.create();
|
||||
if (id) {
|
||||
const idStr = String(id);
|
||||
await stateDPM.findUnique.load(idStr);
|
||||
if (stateDPM.findUnique.data) {
|
||||
setChartData([stateDPM.findUnique.data]);
|
||||
}
|
||||
if (stateDPM.findUnique.data) setChartData([stateDPM.findUnique.data]);
|
||||
}
|
||||
resetForm();
|
||||
router.push("/admin/pendidikan/data-pendidikan");
|
||||
}
|
||||
router.push('/admin/pendidikan/data-pendidikan');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack size={20} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box>
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Title order={4}>Tambah Data Pendidikan</Title>
|
||||
<Stack gap={"xs"}>
|
||||
<TextInput
|
||||
label="Nama"
|
||||
type="text"
|
||||
value={stateDPM.create.form.name}
|
||||
placeholder="Masukkan nama"
|
||||
onChange={(val) => {
|
||||
stateDPM.create.form.name = val.currentTarget.value;
|
||||
<Box px={{ base: 'sm', md: 'xl' }} py="md">
|
||||
<Group mb="md" gap="sm">
|
||||
<Tooltip label="Kembali ke halaman sebelumnya" position="bottom" withArrow>
|
||||
<Button variant="subtle" onClick={() => router.back()} radius="md">
|
||||
<IconArrowBack size={20} stroke={2} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Title order={3} c='black'>Tambah Data Pendidikan</Title>
|
||||
</Group>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
w={{ base: '100%', md: '60%' }}
|
||||
p="lg"
|
||||
radius="md"
|
||||
shadow="xl"
|
||||
bg="white"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Nama Pendidikan"
|
||||
placeholder="Contoh: SD, SMP, SMA"
|
||||
value={stateDPM.create.form.name}
|
||||
onChange={(e) => (stateDPM.create.form.name = e.currentTarget.value)}
|
||||
radius="md"
|
||||
required
|
||||
/>
|
||||
<TextInput
|
||||
label="Jumlah Peserta"
|
||||
placeholder="Masukkan jumlah peserta"
|
||||
value={stateDPM.create.form.jumlah}
|
||||
onChange={(e) => (stateDPM.create.form.jumlah = e.currentTarget.value)}
|
||||
radius="md"
|
||||
type="number"
|
||||
required
|
||||
/>
|
||||
<Group justify="right" mt="sm">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, ${colors['blue-button-trans']})`,
|
||||
color: 'white',
|
||||
boxShadow: '0 0 10px rgba(0,123,255,0.5)',
|
||||
'&:hover': { opacity: 0.9 },
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Jumlah"
|
||||
type="number"
|
||||
value={stateDPM.create.form.jumlah}
|
||||
placeholder="Masukkan jumlah"
|
||||
onChange={(val) => {
|
||||
stateDPM.create.form.jumlah = val.currentTarget.value;
|
||||
}}
|
||||
/>
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
mt={10}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
radius="md"
|
||||
size="md"
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{chartData.length === 0 && (
|
||||
<Text mt="md" c={colors['white-1']} ta="center">
|
||||
Data belum tersedia. Silakan tambahkan data pendidikan baru.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDataPendidikan;
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { Box, Button, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconDatabase, IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
import { Bar, BarChart, Legend, Tooltip as RechartTooltip, ResponsiveContainer, XAxis, YAxis } from 'recharts';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../_com/header';
|
||||
import JudulList from '../../_com/judulList';
|
||||
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
|
||||
import dataPendidikan from '../../_state/pendidikan/data-pendidikan';
|
||||
|
||||
function DataPendidikan() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<Box py="md">
|
||||
<HeaderSearch
|
||||
title='Data Pendidikan'
|
||||
placeholder='pencarian'
|
||||
title="Data Pendidikan"
|
||||
placeholder="Cari data pendidikan..."
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
@@ -29,138 +28,153 @@ function DataPendidikan() {
|
||||
}
|
||||
|
||||
function ListDataPendidikan({ search }: { search: string }) {
|
||||
type DPMrafik = {
|
||||
id: string;
|
||||
name: string;
|
||||
jumlah: number;
|
||||
}
|
||||
type DPMrafik = { id: string; name: string; jumlah: number };
|
||||
const stateDPM = useProxy(dataPendidikan);
|
||||
const [chartData, setChartData] = useState<DPMrafik[]>([]);
|
||||
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
|
||||
const isTablet = useMediaQuery('(max-width: 1024px)')
|
||||
const isMobile = useMediaQuery('(max-width: 768px)')
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [modalHapus, setModalHapus] = useState(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedId) {
|
||||
stateDPM.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
|
||||
stateDPM.findMany.load()
|
||||
stateDPM.delete.byId(selectedId);
|
||||
setModalHapus(false);
|
||||
setSelectedId(null);
|
||||
stateDPM.findMany.load();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
setMounted(true)
|
||||
stateDPM.findMany.load()
|
||||
}, [])
|
||||
setMounted(true);
|
||||
stateDPM.findMany.load();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
if (stateDPM.findMany.data) {
|
||||
setChartData(stateDPM.findMany.data.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
jumlah: Number(item.jumlah)
|
||||
})));
|
||||
setChartData(
|
||||
stateDPM.findMany.data.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
jumlah: Number(item.jumlah),
|
||||
}))
|
||||
);
|
||||
}
|
||||
}, [stateDPM.findMany.data]);
|
||||
|
||||
const filteredData = (stateDPM.findMany.data || []).filter(item => {
|
||||
const filteredData = (stateDPM.findMany.data || []).filter((item) => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.jumlah.toLowerCase().includes(keyword)
|
||||
);
|
||||
return item.name.toLowerCase().includes(keyword) || item.jumlah.toString().includes(keyword);
|
||||
});
|
||||
|
||||
if (!stateDPM.findMany.data) {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton h={500} />
|
||||
<Stack gap="sm">
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Stack gap={'xs'}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Data Pendidikan'
|
||||
href='/admin/pendidikan/data-pendidikan/create'
|
||||
/>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Jumlah</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>{item.jumlah}</TableTd>
|
||||
<TableTd>
|
||||
<Button color='green' onClick={() => router.push(`/admin/pendidikan/data-pendidikan/${item.id}`)}>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button
|
||||
color='red'
|
||||
disabled={stateDPM.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id)
|
||||
setModalHapus(true)
|
||||
}}>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<Box py="sm">
|
||||
<Stack gap="sm">
|
||||
<Paper p="md" withBorder bg={colors['white-1']} shadow="sm" radius="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Daftar Data Pendidikan</Title>
|
||||
<Tooltip label="Tambah Data Pendidikan" withArrow>
|
||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/pendidikan/data-pendidikan/create')}>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
{filteredData.length === 0 ? (
|
||||
<Box py="lg" px="md" style={{ textAlign: 'center' }}>
|
||||
<IconDatabase size={48} stroke={1.5} color={colors['white-1']} />
|
||||
<Text mt="sm" c="dimmed">
|
||||
Tidak ada data pendidikan yang sesuai pencarian
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Table striped highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama</TableTh>
|
||||
<TableTh>Jumlah</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Hapus</TableTh>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.name}</TableTd>
|
||||
<TableTd>{item.jumlah}</TableTd>
|
||||
<TableTd>
|
||||
<Tooltip label="Edit data pendidikan" position="top" withArrow>
|
||||
<Button
|
||||
color="green"
|
||||
variant="light"
|
||||
radius="md"
|
||||
onClick={() => router.push(`/admin/pendidikan/data-pendidikan/${item.id}`)}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Tooltip label="Hapus data pendidikan" position="top" withArrow>
|
||||
<Button
|
||||
color="red"
|
||||
variant="light"
|
||||
radius="md"
|
||||
disabled={stateDPM.delete.loading}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Chart */}
|
||||
{!mounted && !chartData ? (
|
||||
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Title pb={10} order={3}>Grafik Data Pendidikan</Title>
|
||||
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : (
|
||||
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Title pb={10} order={4}>Grafik Data Pendidikan</Title>
|
||||
{mounted && chartData.length > 0 && (
|
||||
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Data Pendidikan" />
|
||||
</BarChart>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
<Paper withBorder p="md" bg={colors['white-1']} shadow="sm" radius="md">
|
||||
<Title order={4} mb="sm">
|
||||
Grafik Data Pendidikan
|
||||
</Title>
|
||||
{mounted && chartData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
<BarChart data={chartData} margin={{ top: 20, right: 30, left: 0, bottom: 5 }}>
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<RechartTooltip />
|
||||
<Legend />
|
||||
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Jumlah Pendidikan" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<Box py="lg" style={{ textAlign: 'center' }}>
|
||||
<IconDatabase size={48} stroke={1.5} color={colors['white-1']} />
|
||||
<Text mt="sm" c="dimmed">
|
||||
Belum ada data untuk ditampilkan
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleDelete}
|
||||
text='Apakah anda yakin ingin menghapus grafik data pendidikan ini?'
|
||||
text="Apakah Anda yakin ingin menghapus data pendidikan ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "Jenjang Pendidikan",
|
||||
value: "jenjangPendidikan",
|
||||
href: "/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan"
|
||||
},
|
||||
{
|
||||
label: "Lembaga",
|
||||
value: "lembaga",
|
||||
href: "/admin/pendidikan/info-sekolah-paud/lembaga"
|
||||
},
|
||||
{
|
||||
label: "Siswa",
|
||||
value: "siswa",
|
||||
href: "/admin/pendidikan/info-sekolah-paud/siswa"
|
||||
},
|
||||
{
|
||||
label: "Pengajar",
|
||||
value: "pengajar",
|
||||
href: "/admin/pendidikan/info-sekolah-paud/pengajar"
|
||||
},
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Info Sekolah & PAUD</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabs;
|
||||
@@ -1,61 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import infoSekolahPaud from '../../../../_state/pendidikan/info-sekolah-paud';
|
||||
|
||||
function CreateJenjangPendidikan() {
|
||||
const router = useRouter();
|
||||
const stateJenjang = useProxy(infoSekolahPaud.jenjangPendidikan)
|
||||
|
||||
useEffect(() => {
|
||||
stateJenjang.findMany.load();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
stateJenjang.create.form = {
|
||||
nama: "",
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await stateJenjang.create.create();
|
||||
resetForm();
|
||||
router.push("/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan")
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={4}>Create Jenjang Pendidikan</Title>
|
||||
<TextInput
|
||||
value={stateJenjang.create.form.nama}
|
||||
onChange={(val) => {
|
||||
stateJenjang.create.form.nama = val.target.value;
|
||||
}}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Jenjang Pendidikan</Text>}
|
||||
placeholder='Masukkan nama jenjang pendidikan'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateJenjangPendidikan;
|
||||
@@ -1,112 +0,0 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import infoSekolahPaud from '../../../_state/pendidikan/info-sekolah-paud';
|
||||
|
||||
|
||||
function JenjangPendidikan() {
|
||||
const [search, setSearch] = useState("")
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Jenjang Pendidikan'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListJenjangPendidikan search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListJenjangPendidikan({ search }: { search: string }) {
|
||||
const stateList = useProxy(infoSekolahPaud.jenjangPendidikan)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const router = useRouter()
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
stateList.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
stateList.findMany.load()
|
||||
}, [])
|
||||
|
||||
const filteredData = (stateList.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.nama.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
|
||||
if (!stateList.findMany.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Jenjang Pendidikan'
|
||||
href='/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan/create'
|
||||
/>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Jenjang Pendidikan</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Delete</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.nama}</TableTd>
|
||||
<TableTd>
|
||||
<Button color="green" onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan/${item.id}`)}>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button color="red" onClick={() => {
|
||||
setSelectedId(item.id)
|
||||
setModalHapus(true)
|
||||
}}>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus jenjang pendidikan ini?'
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default JenjangPendidikan;
|
||||
@@ -1,115 +0,0 @@
|
||||
'use client'
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
import colors from '@/con/colors';
|
||||
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
|
||||
function DetailLembaga() {
|
||||
const detailState = useProxy(infoSekolahPaud.lembagaPendidikan)
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
|
||||
useShallowEffect(() => {
|
||||
detailState.findUnique.load(params?.id as string)
|
||||
}, [])
|
||||
|
||||
|
||||
const handleHapus = () => {
|
||||
if (selectedId) {
|
||||
detailState.delete.byId(selectedId)
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
router.push("/admin/pendidikan/info-sekolah-paud/lembaga")
|
||||
}
|
||||
}
|
||||
|
||||
if (!detailState.findUnique.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={40} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={10}>
|
||||
<Button variant="subtle" onClick={() => router.back()}>
|
||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
||||
<Stack>
|
||||
<Text fz={"xl"} fw={"bold"}>Detail Lembaga</Text>
|
||||
{detailState.findUnique.data ? (
|
||||
<Paper key={detailState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Nama</Text>
|
||||
<Text fz={"lg"}>{detailState.findUnique.data?.nama}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Jenjang Pendidikan</Text>
|
||||
<Text fz={"lg"} >{detailState.findUnique.data?.jenjangPendidikan?.nama}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Jumlah Siswa</Text>
|
||||
<Text fz={"lg"} >{detailState.findUnique.data?.siswa?.length}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"lg"}>Jumlah Pengajar</Text>
|
||||
<Text fz={"lg"} >{detailState.findUnique.data?.pengajar?.length}</Text>
|
||||
</Box>
|
||||
<Flex gap={"xs"} mt={10}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (detailState.findUnique.data) {
|
||||
setSelectedId(detailState.findUnique.data.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={detailState.delete.loading || !detailState.findUnique.data}
|
||||
color={"red"}
|
||||
>
|
||||
<IconX size={20} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (detailState.findUnique.data) {
|
||||
router.push(`/admin/pendidikan/info-sekolah-paud/lembaga/${detailState.findUnique.data.id}/edit`);
|
||||
}
|
||||
}}
|
||||
disabled={!detailState.findUnique.data}
|
||||
color={"green"}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Modal Konfirmasi Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text='Apakah anda yakin ingin menghapus lembaga ini?'
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailLembaga;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user