Compare commits

..

2 Commits

Author SHA1 Message Date
39e1e7b575 QC Tampilan Admin & User, Api berfungsi 2025-09-16 16:47:12 +08:00
4ceea5203f QC Admin - User Menu Ekonomi : Jumlah Pengangguran 2025-09-16 10:11:54 +08:00
138 changed files with 9188 additions and 5312 deletions

View File

@@ -1,51 +1,99 @@
[
{
"month": "Jan",
"year": 2025,
"totalUnemployment": 160,
"educatedUnemployment": 95,
"uneducatedUnemployment": 65,
"percentageChange": null
},
{
"month": "Feb",
"year": 2025,
"totalUnemployment": 155,
"educatedUnemployment": 90,
"uneducatedUnemployment": 65,
"percentageChange": -3.1
},
{
"month": "Mar",
"year": 2025,
"totalUnemployment": 150,
"educatedUnemployment": 88,
"uneducatedUnemployment": 62,
"percentageChange": -3.2
},
{
"month": "Apr",
"year": 2025,
"totalUnemployment": 148,
"educatedUnemployment": 85,
"uneducatedUnemployment": 63,
"percentageChange": -1.3
},
{
"month": "Mei",
"year": 2025,
"totalUnemployment": 145,
"educatedUnemployment": 82,
"uneducatedUnemployment": 63,
"percentageChange": -2.0
},
{
"month": "Jun",
"year": 2025,
"totalUnemployment": 140,
"educatedUnemployment": 80,
"uneducatedUnemployment": 60,
"percentageChange": -3.4
}
]
{
"month": "Jan",
"year": 2025,
"totalUnemployment": 160,
"educatedUnemployment": 95,
"uneducatedUnemployment": 65,
"percentageChange": 0.0
},
{
"month": "Feb",
"year": 2025,
"totalUnemployment": 158,
"educatedUnemployment": 93,
"uneducatedUnemployment": 65,
"percentageChange": -1.25
},
{
"month": "Mar",
"year": 2025,
"totalUnemployment": 155,
"educatedUnemployment": 91,
"uneducatedUnemployment": 64,
"percentageChange": -1.90
},
{
"month": "Apr",
"year": 2025,
"totalUnemployment": 152,
"educatedUnemployment": 89,
"uneducatedUnemployment": 63,
"percentageChange": -1.94
},
{
"month": "Mei",
"year": 2025,
"totalUnemployment": 150,
"educatedUnemployment": 88,
"uneducatedUnemployment": 62,
"percentageChange": -1.32
},
{
"month": "Jun",
"year": 2025,
"totalUnemployment": 148,
"educatedUnemployment": 87,
"uneducatedUnemployment": 61,
"percentageChange": -1.33
},
{
"month": "Jul",
"year": 2025,
"totalUnemployment": 145,
"educatedUnemployment": 85,
"uneducatedUnemployment": 60,
"percentageChange": -2.03
},
{
"month": "Agu",
"year": 2025,
"totalUnemployment": 142,
"educatedUnemployment": 84,
"uneducatedUnemployment": 58,
"percentageChange": -2.07
},
{
"month": "Sep",
"year": 2025,
"totalUnemployment": 140,
"educatedUnemployment": 83,
"uneducatedUnemployment": 57,
"percentageChange": -1.41
},
{
"month": "Okt",
"year": 2025,
"totalUnemployment": 138,
"educatedUnemployment": 82,
"uneducatedUnemployment": 56,
"percentageChange": -1.43
},
{
"month": "Nov",
"year": 2025,
"totalUnemployment": 135,
"educatedUnemployment": 80,
"uneducatedUnemployment": 55,
"percentageChange": -2.17
},
{
"month": "Des",
"year": 2025,
"totalUnemployment": 132,
"educatedUnemployment": 78,
"uneducatedUnemployment": 54,
"percentageChange": -2.22
}
]

View File

@@ -170,7 +170,7 @@ model KategoriDesaAntiKorupsi {
//========================================= SDGS Desa ========================================= //
model SdgsDesa {
id String @id @default(cuid())
name String @unique
name String
jumlah String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
@@ -183,7 +183,7 @@ model SdgsDesa {
//========================================= APBDes ========================================= //
model APBDes {
id String @id @default(cuid())
name String @unique
name String
jumlah String
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
imageId String?
@@ -198,7 +198,7 @@ model APBDes {
//========================================= PRESTASI DESA ========================================= //
model PrestasiDesa {
id String @id @default(cuid())
name String @unique
name String
deskripsi String @db.Text
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
kategoriId String
@@ -1416,6 +1416,9 @@ model PosisiOrganisasi {
pegawai Pegawai[]
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posisi_organisasi")
}
@@ -1484,7 +1487,7 @@ model ProgramKemiskinan {
id String @id @default(uuid())
nama String
deskripsi String
ikonUrl String?
icon String
isActive Boolean @default(true)
statistikId String? @unique
statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id])
@@ -1566,7 +1569,7 @@ model DataDemografiPekerjaan {
model DetailDataPengangguran {
id String @id @default(uuid()) @db.Uuid
month String @db.VarChar(20)
year DateTime
year Int
totalUnemployment Int
educatedUnemployment Int
uneducatedUnemployment Int

View File

@@ -376,8 +376,7 @@ import fileStorage from "./data/file-storage.json";
for (const l of apbdes) {
await prisma.aPBDes.upsert({
where: {
name: l.name,
jumlah: l.jumlah,
id: l.id,
},
update: {
name: l.name,
@@ -895,12 +894,9 @@ import fileStorage from "./data/file-storage.json";
console.log("hubungan organisasi success ...");
for (const d of detailDataPengangguran) {
// Convert the year to a Date object (using January 1st of the year as the date)
const yearAsDate = new Date(d.year, 0, 1);
await prisma.detailDataPengangguran.upsert({
where: {
month_year: { month: d.month, year: yearAsDate },
month_year: { month: d.month, year: d.year },
},
update: {
totalUnemployment: d.totalUnemployment,
@@ -910,7 +906,7 @@ import fileStorage from "./data/file-storage.json";
},
create: {
month: d.month,
year: yearAsDate,
year: d.year,
totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment,

BIN
public/perbekel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

View File

@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import React from 'react'
import {
IconLeaf,
IconTrophy,
IconTent,
IconChartLine,
IconRecycle,
IconTruck,
IconScale,
IconClipboard,
IconTrash,
IconHomeEco,
IconChristmasTreeFilled,
IconTrendingUp,
IconShieldFilled,
IconHome,
IconTree,
IconDroplet,
IconCash,
IconSchool,
IconShoppingCart,
IconHospital,
} from '@tabler/icons-react'
export type IconKey =
| 'ekowisata'
| 'kompetisi'
| 'wisata'
| 'ekonomi'
| 'sampah'
| 'truck'
| 'scale'
| 'clipboard'
| 'trash'
| 'lingkunganSehat'
| 'sumberOksigen'
| 'ekonomiBerkelanjutan'
| 'mencegahBencana'
| 'rumah'
| 'pohon'
| 'air'
| 'bantuan'
| 'pelatihan'
| 'subsidi'
| 'layananKesehatan'
const iconMap: Record<IconKey, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
rumah: IconHome,
pohon: IconTree,
air: IconDroplet,
bantuan: IconCash,
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital,
}
type Props = {
name: IconKey
size?: number
color?: string
}
export const IconMapper: React.FC<Props> = ({ name, size = 24, color }) => {
const IconComponent = iconMap[name]
if (!IconComponent) return null
return <IconComponent size={size} color={color} />
}

View File

@@ -3,16 +3,20 @@
import { Box, rem, Select } from '@mantine/core';
import {
IconCash,
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDroplet,
IconHome,
IconHomeEco,
IconHospital,
IconLeaf,
IconRecycle,
IconScale,
IconSchool,
IconShieldFilled,
IconShoppingCart,
IconTent,
IconTrashFilled,
IconTree,
@@ -32,13 +36,17 @@ const iconMap = {
scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled },
lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco},
sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled},
ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp},
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
rumah: {label: 'Rumah', icon: IconHome},
pohon: {label: 'Pohon', icon: IconTree},
air: {label: 'Air', icon: IconDroplet}
lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco },
sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled },
ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp },
mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled },
rumah: { label: 'Rumah', icon: IconHome },
pohon: { label: 'Pohon', icon: IconTree },
air: { label: 'Air', icon: IconDroplet },
bantuan: { label: 'Bantuan', icon: IconCash },
pelatihan: { label: 'Pelatihan', icon: IconSchool },
subsidi: { label: 'Subsidi', icon: IconShoppingCart },
layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
};

View File

@@ -2,16 +2,20 @@
import { Box, rem, Select } from '@mantine/core';
import {
IconCash,
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDroplet,
IconHome,
IconHomeEco,
IconHospital,
IconLeaf,
IconRecycle,
IconScale,
IconSchool,
IconShieldFilled,
IconShoppingCart,
IconTent,
IconTrashFilled,
IconTree,
@@ -36,7 +40,11 @@ const iconMap = {
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
rumah: {label: 'Rumah', icon: IconHome},
pohon: {label: 'Pohon', icon: IconTree},
air: {label: 'Air', icon: IconDroplet}
air: {label: 'Air', icon: IconDroplet},
bantuan: {label: 'Bantuan', icon: IconCash},
pelatihan: {label: 'Pelatihan', icon: IconSchool},
subsidi: {label: 'Subsidi', icon: IconShoppingCart},
layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital},
};
type IconKey = keyof typeof iconMap;

View File

@@ -7,9 +7,13 @@ import { z } from "zod";
const templateApbDesa = z.object({
tahun: z.number().min(4, "Tahun minimal 4 karakter"),
pembiayaanIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 pembiayaan"),
pembiayaanIds: z
.array(z.string().uuid())
.nonempty("Pilih minimal 1 pembiayaan"),
belanjaIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 belanja"),
pendapatanIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 pendapatan"),
pendapatanIds: z
.array(z.string().uuid())
.nonempty("Pilih minimal 1 pendapatan"),
});
const ApbDesaDefaultForm = {
@@ -54,32 +58,46 @@ const ApbDesa = proxy({
},
},
findMany: {
data: null as
| Prisma.ApbDesaGetPayload<{
include: {
pendapatan: true;
belanja: true;
pembiayaan: true;
};
}>[]
| null,
data: null as
| Prisma.ApbDesaGetPayload<{
include: {
pendapatan: true;
belanja: true;
pembiayaan: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
async load() {
search: "",
load: async (page = 1, limit = 10, search = "") => {
ApbDesa.findMany.loading = true; // ✅ Akses langsung via nama path
ApbDesa.findMany.page = page;
ApbDesa.findMany.search = search;
try {
this.loading = true;
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
"find-many"
].get();
if (res.status === 200) {
this.data = res.data?.data ?? [];
].get({ query });
if (res.status === 200 && res.data?.success) {
ApbDesa.findMany.data = res.data.data ?? [];
ApbDesa.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
toast.error(res.data?.message || "Gagal mengambil APB Desa");
ApbDesa.findMany.data = [];
ApbDesa.findMany.totalPages = 1;
}
} catch (error) {
console.error("Find many error:", error);
toast.error("Gagal mengambil APB Desa");
} catch (err) {
console.error("Gagal fetch APB Desa paginated:", err);
ApbDesa.findMany.data = [];
ApbDesa.findMany.totalPages = 1;
} finally {
this.loading = false;
ApbDesa.findMany.loading = false;
}
},
},
@@ -106,13 +124,13 @@ const ApbDesa = proxy({
throw new Error("Gagal mengambil APB Desa");
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || "Gagal memuat APB Desa");
}
const data = result.data;
this.id = id;
this.form = {
tahun: data.tahun || 0,
@@ -120,7 +138,7 @@ const ApbDesa = proxy({
belanjaIds: data.belanja?.map((b: any) => b.id) || [],
pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
};
return data;
} catch (error) {
console.error("Error loading APB Desa:", error);
@@ -189,7 +207,7 @@ const ApbDesa = proxy({
data: null as Prisma.ApbDesaGetPayload<{
include: { pendapatan: true; belanja: true; pembiayaan: true };
}> | null,
async load(id: string) {
try {
const response = await fetch(
@@ -199,11 +217,11 @@ const ApbDesa = proxy({
throw new Error("Gagal mengambil detail APB Desa");
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || "Gagal mengambil data");
}
this.data = result.data; // ✅ fix utama di sini
return result.data;
} catch (error) {
@@ -264,34 +282,32 @@ const pendapatan = proxy({
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
pendapatan.findMany.loading = true; // Use the full path to access the property
search: "",
load: async (page = 1, limit = 10, search = "") => {
pendapatan.findMany.loading = true; // ✅ Akses langsung via nama path
pendapatan.findMany.page = page;
pendapatan.findMany.search = search;
try {
const res =
await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[
"find-many"
].get({
query: { page, limit },
});
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
pendapatan.findMany.data = res.data.data || [];
pendapatan.findMany.total = res.data.total || 0;
pendapatan.findMany.totalPages = res.data.totalPages || 1;
pendapatan.findMany.data = res.data.data ?? [];
pendapatan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
console.error("Failed to load pendapatan:", res.data?.message);
pendapatan.findMany.data = [];
pendapatan.findMany.total = 0;
pendapatan.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pendapatan:", error);
} catch (err) {
console.error("Gagal fetch pendapatan asli desa paginated:", err);
pendapatan.findMany.data = [];
pendapatan.findMany.total = 0;
pendapatan.findMany.totalPages = 1;
} finally {
pendapatan.findMany.loading = false;
@@ -308,12 +324,15 @@ const pendapatan = proxy({
return null;
}
try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -349,16 +368,19 @@ const pendapatan = proxy({
try {
pendapatan.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
value: this.form.value,
}),
});
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
value: this.form.value,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
@@ -495,23 +517,37 @@ const belanja = proxy({
name: string;
value: number;
}>,
page: 1,
totalPages: 1,
loading: false,
async load() {
search: "",
load: async (page = 1, limit = 10, search = "") => {
belanja.findMany.loading = true; // ✅ Akses langsung via nama path
belanja.findMany.page = page;
belanja.findMany.search = search;
try {
this.loading = true;
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[
"find-many"
].get();
if (res.status === 200) {
this.data = res.data?.data ?? [];
].get({ query });
if (res.status === 200 && res.data?.success) {
belanja.findMany.data = res.data.data ?? [];
belanja.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
toast.error(res.data?.message || "Gagal mengambil Belanja");
belanja.findMany.data = [];
belanja.findMany.totalPages = 1;
}
} catch (error) {
console.error("Find many error:", error);
toast.error("Gagal mengambil Belanja");
} catch (err) {
console.error("Gagal fetch Belanja paginated:", err);
belanja.findMany.data = [];
belanja.findMany.totalPages = 1;
} finally {
this.loading = false;
belanja.findMany.loading = false;
}
},
},
@@ -525,12 +561,15 @@ const belanja = proxy({
return null;
}
try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/belanja/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -566,16 +605,19 @@ const belanja = proxy({
try {
belanja.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
value: this.form.value,
}),
});
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
value: this.form.value,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
@@ -710,23 +752,37 @@ const pembiayaan = proxy({
name: string;
value: number;
}>,
page: 1,
totalPages: 1,
loading: false,
async load() {
search: "",
load: async (page = 1, limit = 10, search = "") => {
pembiayaan.findMany.loading = true; // ✅ Akses langsung via nama path
pembiayaan.findMany.page = page;
pembiayaan.findMany.search = search;
try {
this.loading = true;
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[
"find-many"
].get();
if (res.status === 200) {
this.data = res.data?.data ?? [];
].get({ query });
if (res.status === 200 && res.data?.success) {
pembiayaan.findMany.data = res.data.data ?? [];
pembiayaan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
toast.error(res.data?.message || "Gagal mengambil Pembiayaan");
pembiayaan.findMany.data = [];
pembiayaan.findMany.totalPages = 1;
}
} catch (error) {
console.error("Find many error:", error);
toast.error("Gagal mengambil Pembiayaan");
} catch (err) {
console.error("Gagal fetch Pembiayaan paginated:", err);
pembiayaan.findMany.data = [];
pembiayaan.findMany.totalPages = 1;
} finally {
this.loading = false;
pembiayaan.findMany.loading = false;
}
},
},
@@ -740,12 +796,15 @@ const pembiayaan = proxy({
return null;
}
try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -781,16 +840,19 @@ const pembiayaan = proxy({
try {
pembiayaan.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
value: this.form.value,
}),
});
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
value: this.form.value,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(

View File

@@ -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";
@@ -71,12 +72,37 @@ const demografiPekerjaan = proxy({
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.ekonomi.demografipekerjaan[
"find-many"
].get();
if (res.status === 200) {
demografiPekerjaan.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
demografiPekerjaan.findMany.loading = true; // ✅ Akses langsung via nama path
demografiPekerjaan.findMany.page = page;
demografiPekerjaan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.demografipekerjaan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
demografiPekerjaan.findMany.data = res.data.data ?? [];
demografiPekerjaan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
demografiPekerjaan.findMany.data = [];
demografiPekerjaan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch demografi pekerjaan paginated:", err);
demografiPekerjaan.findMany.data = [];
demografiPekerjaan.findMany.totalPages = 1;
} finally {
demografiPekerjaan.findMany.loading = false;
}
},
},
@@ -194,4 +220,4 @@ const demografiPekerjaan = proxy({
},
},
});
export default demografiPekerjaan
export default demografiPekerjaan;

View File

@@ -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";
@@ -69,16 +70,37 @@ const jumlahPendudukMiskin = proxy({
select: { id: true; year: true; totalPoorPopulation: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"find-many"
].get();
if (res.status === 200) {
jumlahPendudukMiskin.findMany.data = res.data?.data ?? [];
}
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
jumlahPendudukMiskin.findMany.loading = true; // ✅ Akses langsung via nama path
jumlahPendudukMiskin.findMany.page = page;
jumlahPendudukMiskin.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
jumlahPendudukMiskin.findMany.data = res.data.data ?? [];
jumlahPendudukMiskin.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jumlahPendudukMiskin.findMany.data = [];
jumlahPendudukMiskin.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jumlah penduduk miskin paginated:", err);
jumlahPendudukMiskin.findMany.data = [];
jumlahPendudukMiskin.findMany.totalPages = 1;
} finally {
jumlahPendudukMiskin.findMany.loading = false;
}
},
},
},
findUnique: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true };

View File

@@ -15,7 +15,8 @@ const templateJumlahPengngguran = z.object({
uneducatedUnemployment: z
.number()
.min(1, "Pengangguran tidak pendidikan harus diisi"),
percentageChange: z.number().min(0, "Persentase perubahan harus diisi"),
percentageChange: z.number({ invalid_type_error: "Persentase perubahan harus angka" }),
});
type JumlahPengangguran = {
@@ -29,7 +30,7 @@ type JumlahPengangguran = {
const jumlahPengangguranForm: JumlahPengangguran = {
month: "",
year: 0,
year: new Date().getFullYear(), // Default to current year
totalUnemployment: 0,
educatedUnemployment: 0,
uneducatedUnemployment: 0,
@@ -60,13 +61,21 @@ const jumlahPengangguran = proxy({
form: jumlahPengangguranForm,
loading: false,
async create() {
const cek = templateJumlahPengngguran.safeParse(
jumlahPengangguran.create.form
);
// Ensure all number fields are actual numbers
const formData = {
...jumlahPengangguran.create.form,
year: Number(jumlahPengangguran.create.form.year) || new Date().getFullYear(),
totalUnemployment: Number(jumlahPengangguran.create.form.totalUnemployment) || 0,
educatedUnemployment: Number(jumlahPengangguran.create.form.educatedUnemployment) || 0,
uneducatedUnemployment: Number(jumlahPengangguran.create.form.uneducatedUnemployment) || 0,
percentageChange: Number(jumlahPengangguran.create.form.percentageChange) || 0,
};
const cek = templateJumlahPengngguran.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
.map((v) => `${v.path.join(".")} (${v.message})`)
.join("\n")}]`;
toast.error(err);
return null;
}
@@ -78,7 +87,7 @@ const jumlahPengangguran = proxy({
].post(jumlahPengangguran.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
const id = res.data?.id;
if (id) {
toast.success("Success create");
jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
@@ -103,16 +112,40 @@ const jumlahPengangguran = proxy({
omit: { isActive: true };
}>[]
| null,
async load() {
const res =
await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
"find-many"
].get();
if (res.status === 200) {
jumlahPengangguran.findMany.data = res.data?.data ?? [];
}
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
jumlahPengangguran.findMany.loading = true; // ✅ Akses langsung via nama path
jumlahPengangguran.findMany.page = page;
jumlahPengangguran.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jumlahPengangguran.findMany.data = res.data.data ?? [];
jumlahPengangguran.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
jumlahPengangguran.findMany.data = [];
jumlahPengangguran.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jumlah pengangguran paginated:", err);
jumlahPengangguran.findMany.data = [];
jumlahPengangguran.findMany.totalPages = 1;
} finally {
jumlahPengangguran.findMany.loading = false;
}
},
},
},
findUnique: {
data: null as Prisma.DetailDataPengangguranGetPayload<{

View File

@@ -8,7 +8,7 @@ import { z } from "zod";
const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
ikonUrl: z.string().optional(),
icon: z.string().min(1, "Icon minimal 1 karakter"),
statistik: z.object({
tahun: z.string().min(1, "Tahun minimal 1 karakter"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
@@ -18,7 +18,7 @@ const templateForm = z.object({
const defaultForm = {
nama: "",
deskripsi: "",
ikonUrl: "",
icon: "",
statistik: {
tahun: "",
jumlah: "",
@@ -148,7 +148,7 @@ const programKemiskinanState = proxy({
this.form = {
nama: data.nama,
deskripsi: data.deskripsi,
ikonUrl: data.ikonUrl || "",
icon: data.icon,
statistik: {
tahun: data.statistik.tahun,
jumlah: data.statistik.jumlah,
@@ -189,7 +189,7 @@ const programKemiskinanState = proxy({
body: JSON.stringify({
nama: this.form.nama,
deskripsi: this.form.deskripsi,
ikonUrl: this.form.ikonUrl,
icon: this.form.icon,
statistik: {
tahun: this.form.statistik.tahun,
jumlah: this.form.statistik.jumlah,

View File

@@ -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";
@@ -76,13 +77,37 @@ const grafikSektorUnggulan = proxy({
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[
"find-many"
].get();
if (res.status === 200) {
grafikSektorUnggulan.findMany.data = res.data?.data ?? [];
search: "",
load: async (page = 1, limit = 10, search = "") => {
grafikSektorUnggulan.findMany.loading = true; // ✅ Akses langsung via nama path
grafikSektorUnggulan.findMany.page = page;
grafikSektorUnggulan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
grafikSektorUnggulan.findMany.data = res.data.data ?? [];
grafikSektorUnggulan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
grafikSektorUnggulan.findMany.data = [];
grafikSektorUnggulan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch sektor unggulan desa paginated:", err);
grafikSektorUnggulan.findMany.data = [];
grafikSektorUnggulan.findMany.totalPages = 1;
} finally {
grafikSektorUnggulan.findMany.loading = false;
}
},
},

View File

@@ -161,18 +161,34 @@ const posisiOrganisasi = proxy({
deskripsi: string | null;
hierarki: number;
}>,
async load() {
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search;
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"posisi-organisasi"
]["find-many"].get();
if (res.status === 200) {
// The API now returns the id field, so we can use it directly
this.data = res.data?.data ?? [];
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
} else {
posisiOrganisasi.findMany.data = [];
posisiOrganisasi.findMany.totalPages = 1;
}
} catch (error) {
console.error("Find many error:", error);
this.data = [];
} catch (err) {
console.error("Gagal fetch posisi organisasi paginated:", err);
posisiOrganisasi.findMany.data = [];
posisiOrganisasi.findMany.totalPages = 1;
} finally {
posisiOrganisasi.findMany.loading = false;
}
},
},

View File

@@ -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";
@@ -75,16 +76,37 @@ const grafikBerdasarkanUsiaKerjaNganggur = proxy({
omit: { isActive: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data?.data ?? [];
}
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = true; // ✅ Akses langsung via nama path
grafikBerdasarkanUsiaKerjaNganggur.findMany.page = page;
grafikBerdasarkanUsiaKerjaNganggur.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data.data ?? [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = res.data.totalPages ?? 1;
} else {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch grafik berdasarkan usia kerja yang menganggur paginated:", err);
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = 1;
} finally {
grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = false;
}
},
},
},
findUnique: {
data: null as Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
omit: { isActive: true };
@@ -259,15 +281,36 @@ const grafikBerdasarkanPendidikan = proxy({
omit: { isActive: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanPendidikan.findMany.data = res.data?.data ?? [];
}
},
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
grafikBerdasarkanPendidikan.findMany.loading = true; // ✅ Akses langsung via nama path
grafikBerdasarkanPendidikan.findMany.page = page;
grafikBerdasarkanPendidikan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanPendidikan.findMany.data = res.data.data ?? [];
grafikBerdasarkanPendidikan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
grafikBerdasarkanPendidikan.findMany.data = [];
grafikBerdasarkanPendidikan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch grafik berdasarkan pendidikan paginated:", err);
grafikBerdasarkanPendidikan.findMany.data = [];
grafikBerdasarkanPendidikan.findMany.totalPages = 1;
} finally {
grafikBerdasarkanPendidikan.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{

View File

@@ -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";
@@ -97,13 +98,37 @@ const laporanPublikState = proxy({
include: { penanganan: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.laporanpublik["find-many"].get();
if (res.status === 200) {
laporanPublikState.findMany.data = res.data?.data ?? [];
}
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
laporanPublikState.findMany.loading = true; // ✅ Akses langsung via nama path
laporanPublikState.findMany.page = page;
laporanPublikState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan.laporanpublik["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
laporanPublikState.findMany.data = res.data.data ?? [];
laporanPublikState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
laporanPublikState.findMany.data = [];
laporanPublikState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch laporan publik paginated:", err);
laporanPublikState.findMany.data = [];
laporanPublikState.findMany.totalPages = 1;
} finally {
laporanPublikState.findMany.loading = false;
}
},
},
},
findUnique: {
data: null as Prisma.LaporanPublikGetPayload<{
include: { penanganan: true };

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
@@ -62,43 +62,50 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
<Tabs
color={colors['blue-button']}
variant='pills'
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",
}}
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconNews, IconCategory } from '@tabler/icons-react';
@@ -49,43 +49,50 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
<Tabs
color={colors['blue-button']}
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",
}}
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -96,9 +96,11 @@ function ListBerita({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed">

View File

@@ -110,7 +110,7 @@ export default function ListImage() {
radius="md"
onClick={() => {
stateFileStorage
.del({ name: v.name })
.del({ id: v.id })
.finally(() => toast("Foto berhasil dihapus"));
}}
>
@@ -143,14 +143,15 @@ export default function ListImage() {
<Flex justify="center">
<Pagination
total={total}
value={stateFileStorage.page} // Changed from page to value
size="md"
radius="md"
withEdges
onChange={(page) => {
stateFileStorage.page = page;
stateFileStorage.load();
stateFileStorage.load({ page });
}}
/>
</Flex>
)}
</Stack>

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconPhoto, IconVideo } from '@tabler/icons-react';
@@ -48,43 +48,50 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
<Tabs
color={colors['blue-button']}
variant='pills'
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",
}}
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconListDetails, IconCategory } from '@tabler/icons-react';
@@ -48,37 +48,44 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Pengumuman</Title>
<Tabs
color={colors['blue-button']}
variant='pills'
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>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{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>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconCategory, IconListCheck } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -48,37 +48,44 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Potensi</Title>
<Tabs
color={colors['blue-button']}
variant='pills'
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>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{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>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
@@ -55,56 +55,63 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
<Stack gap="lg">
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
<Tabs
color={colors['blue-button']}
variant='pills'
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}
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel>
))}
</Tabs>
</Stack>
);
{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>
</ScrollArea>
{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)",
}}
>
{/* Konten dummy, bisa diganti sesuai routing */}
<>{children}</>
</TabsPanel>
))}
</Tabs>
</Stack>
);
}
export default LayoutTabsDetail;
export default LayoutTabsDetail;

View File

@@ -1,73 +1,150 @@
/* 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,
ScrollArea,
} from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import {
IconFileAnalytics,
IconCoins,
IconShoppingCart,
IconWallet,
} 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: "APB Desa",
value: "apbdesa",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa",
icon: <IconFileAnalytics size={18} stroke={1.8} />,
tooltip: "Lihat ringkasan Anggaran Pendapatan dan Belanja Desa",
},
{
label: "Pendapatan",
value: "pendapatan",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan"
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan",
icon: <IconCoins size={18} stroke={1.8} />,
tooltip: "Kelola data pendapatan desa",
},
{
label: "Belanja",
value: "belanja",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja"
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja",
icon: <IconShoppingCart size={18} stroke={1.8} />,
tooltip: "Atur data belanja desa",
},
{
label: "Pembiayaan",
value: "pembiayaan",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan"
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan",
icon: <IconWallet size={18} stroke={1.8} />,
tooltip: "Kelola data pembiayaan 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}>Pendapatan Asli 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 */}
<></>
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Pendapatan Asli Desa
</Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{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",
flexShrink: 0,
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{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;

View File

@@ -3,7 +3,19 @@
/* eslint-disable react-hooks/exhaustive-deps */
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, MultiSelect, Paper, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
MultiSelect,
Paper,
Skeleton,
Stack,
Text,
TextInput,
Title,
Tooltip,
Group,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -23,7 +35,7 @@ function EditAPBDesa() {
pembiayaanIds: apbState.update.form.pembiayaanIds || [],
});
// Load APB desa by id saat pertama kali
// Load APB desa by id
useEffect(() => {
const loadAPBdesa = async () => {
const id = params?.id as string;
@@ -46,12 +58,10 @@ function EditAPBDesa() {
};
loadAPBdesa();
}, [params?.id]); // ✅ hapus beritaState dari dependency
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state with form data
apbState.update.form = {
...apbState.update.form,
tahun: Number(formData.tahun),
@@ -70,65 +80,95 @@ function EditAPBDesa() {
};
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 APB Desa</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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 APB Desa
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Tahun */}
<TextInput
type='number'
type="number"
value={formData.tahun}
onChange={(val) => {
setFormData({ ...formData, tahun: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="masukkan tahun"
onChange={(e) =>
setFormData({ ...formData, tahun: e.target.value })
}
label={<Text fz="sm" fw="bold">Tahun</Text>}
placeholder="Masukkan tahun anggaran"
required
/>
{/* Selects */}
<SelectPendapatan
selectedIds={formData.pendapatanIds}
onSelectionChange={(ids) => {
setFormData({ ...formData, pendapatanIds: ids });
}}
onSelectionChange={(ids) =>
setFormData({ ...formData, pendapatanIds: ids })
}
/>
<SelectBelanja
selectedIds={formData.belanjaIds}
onSelectionChange={(ids) => {
setFormData({ ...formData, belanjaIds: ids });
}}
onSelectionChange={(ids) =>
setFormData({ ...formData, belanjaIds: ids })
}
/>
<SelectPembiayaan
selectedIds={formData.pembiayaanIds}
onSelectionChange={(ids) => {
setFormData({ ...formData, pembiayaanIds: ids });
}}
onSelectionChange={(ids) =>
setFormData({ ...formData, pembiayaanIds: ids })
}
/>
<Button onClick={handleSubmit}>Simpan</Button>
{/* Save 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>
);
/* --- Sub Components --- */
/* Select Pendapatan */
interface SelectPendapatanProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectPendapatan({
selectedIds = [],
onSelectionChange,
}: SelectPendapatanProps) {
function SelectPendapatan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
useShallowEffect(() => {
pendapatanState.findMany.load().then(() => {
console.log("Pendapatan berhasil dimuat:", pendapatanState.findMany.data);
});
pendapatanState.findMany.load();
}, []);
if (!pendapatanState.findMany.data) {
@@ -137,10 +177,10 @@ function EditAPBDesa() {
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pendapatan</Text>}
data={pendapatanState.findMany.data.map(p => ({
label={<Text fz="sm" fw="bold">Pendapatan</Text>}
data={pendapatanState.findMany.data.map((p: any) => ({
value: p.id,
label: p.name
label: p.name,
}))}
value={selectedIds}
onChange={onSelectionChange}
@@ -152,22 +192,11 @@ function EditAPBDesa() {
);
}
/* Select Belanja */
interface SelectBelanjaProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectBelanja({
selectedIds = [],
onSelectionChange,
}: SelectBelanjaProps) {
function SelectBelanja({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
useShallowEffect(() => {
belanjaState.findMany.load().then(() => {
console.log("Belanja berhasil dimuat:", belanjaState.findMany.data);
});
belanjaState.findMany.load();
}, []);
if (!belanjaState.findMany.data) {
@@ -176,10 +205,10 @@ function EditAPBDesa() {
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Belanja</Text>}
data={belanjaState.findMany.data.map(b => ({
label={<Text fz="sm" fw="bold">Belanja</Text>}
data={belanjaState.findMany.data.map((b: any) => ({
value: b.id,
label: b.name
label: b.name,
}))}
value={selectedIds}
onChange={onSelectionChange}
@@ -191,22 +220,11 @@ function EditAPBDesa() {
);
}
/* Select Pembiayaan */
interface SelectPembiayaanProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectPembiayaan({
selectedIds = [],
onSelectionChange,
}: SelectPembiayaanProps) {
function SelectPembiayaan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
useShallowEffect(() => {
pembiayaanState.findMany.load().then(() => {
console.log("Pembiayaan berhasil dimuat:", pembiayaanState.findMany.data);
});
pembiayaanState.findMany.load();
}, []);
if (!pembiayaanState.findMany.data) {
@@ -215,10 +233,10 @@ function EditAPBDesa() {
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pembiayaan</Text>}
data={pembiayaanState.findMany.data.map(b => ({
value: b.id,
label: b.name
label={<Text fz="sm" fw="bold">Pembiayaan</Text>}
data={pembiayaanState.findMany.data.map((p: any) => ({
value: p.id,
label: p.name,
}))}
value={selectedIds}
onChange={onSelectionChange}

View File

@@ -2,148 +2,204 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
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 DetailAPBDesa() {
const apbState = useProxy(PendapatanAsliDesa.ApbDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
const apbState = useProxy(PendapatanAsliDesa.ApbDesa);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
console.log("PARAM ID:", params?.id)
apbState.findUnique.load(params?.id as string)
}, [])
apbState.findUnique.load(params?.id as string);
}, []);
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
const formatRupiah = (value: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
const handleHapus = () => {
if (selectedId) {
apbState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa")
apbState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push(
'/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'
);
}
}
};
if (!apbState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = apbState.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 APB Desa</Text>
{apbState.findUnique.data ? (
<Paper key={apbState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Tahun</Text>
<Text fz={"lg"}>{apbState.findUnique.data?.tahun}</Text>
</Box>
<Box>
<Stack gap={"xs"}>
<Text fw={"bold"} fz={"lg"}>Detail Pembiayaan</Text>
{(apbState.findUnique.data?.pembiayaan || []).map((item) => (
<Text fz={"lg"} key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text>
))}
<Text fz={"lg"} fw={"bold"}>
Total: {formatRupiah((apbState.findUnique.data?.pembiayaan || [])
.reduce((sum, item) => sum + Number(item.value), 0))}
<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%', lg: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail APB Desa
</Text>
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">
Tahun
</Text>
<Text fz="md" c="dimmed">
{data.tahun}
</Text>
</Box>
<Box>
<Stack gap="xs">
<Text fw="bold" fz="lg">
Detail Pembiayaan
</Text>
{(data?.pembiayaan || []).map((item) => (
<Text fz="md" c="dimmed" key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text>
</Stack>
</Box>
<Box>
<Stack gap={"xs"}>
<Text fw={"bold"} fz={"lg"}>Detail Belanja</Text>
{(apbState.findUnique.data?.belanja || []).map((item) => (
<Text fz={"lg"} key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text>
))}
<Text fz={"lg"} fw={"bold"}>
Total: {formatRupiah((apbState.findUnique.data?.belanja || [])
.reduce((sum, item) => sum + Number(item.value), 0))}
))}
<Text fz="md" fw="bold">
Total:{' '}
{formatRupiah(
(data?.pembiayaan || []).reduce(
(sum, item) => sum + Number(item.value),
0
)
)}
</Text>
</Stack>
</Box>
<Box>
<Stack gap="xs">
<Text fw="bold" fz="lg">
Detail Belanja
</Text>
{(data?.belanja || []).map((item) => (
<Text fz="md" c="dimmed" key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text>
</Stack>
</Box>
<Box>
<Stack gap={"xs"}>
<Text fw={"bold"} fz={"lg"}>Detail Pendapatan</Text>
{(apbState.findUnique.data?.pendapatan || []).map((item) => (
<Text fz={"lg"} key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text>
))}
<Text fz={"lg"} fw={"bold"}>
Total: {formatRupiah((apbState.findUnique.data?.pendapatan || [])
.reduce((sum, item) => sum + Number(item.value), 0))}
))}
<Text fz="md" fw="bold">
Total:{' '}
{formatRupiah(
(data?.belanja || []).reduce(
(sum, item) => sum + Number(item.value),
0
)
)}
</Text>
</Stack>
</Box>
<Box>
<Stack gap="xs">
<Text fw="bold" fz="lg">
Detail Pendapatan
</Text>
{(data?.pendapatan || []).map((item) => (
<Text fz="md" c="dimmed" key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text>
</Stack>
</Box>
<Flex gap={"xs"} mt={10}>
))}
<Text fz="md" fw="bold">
Total:{' '}
{formatRupiah(
(data?.pendapatan || []).reduce(
(sum, item) => sum + Number(item.value),
0
)
)}
</Text>
</Stack>
</Box>
<Group gap="sm" mt={10}>
<Tooltip label="Hapus APB Desa" withArrow position="top">
<Button
color="red"
onClick={() => {
if (apbState.findUnique.data) {
setSelectedId(apbState.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={apbState.delete.loading || !apbState.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit APB Desa" withArrow position="top">
<Button
onClick={() => {
if (apbState.findUnique.data) {
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${apbState.findUnique.data.id}/edit`);
}
}}
disabled={!apbState.findUnique.data}
color={"green"}
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${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 APB Desa ini?'
text="Apakah Anda yakin ingin menghapus APB Desa ini?"
/>
</Box>
);

View File

@@ -1,15 +1,28 @@
'use client'
'use client';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, MultiSelect, Paper, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
MultiSelect,
Paper,
Skeleton,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateAPBDesa() {
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
const router = useRouter()
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter();
const resetForm = () => {
apbDesaState.create.form = {
@@ -17,74 +30,101 @@ function CreateAPBDesa() {
pendapatanIds: [],
belanjaIds: [],
pembiayaanIds: [],
}
}
};
};
const handleSubmit = async () => {
await apbDesaState.create.submit()
resetForm()
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa")
}
await apbDesaState.create.submit();
resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
};
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"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create APB Desa</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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 APB Desa
</Title>
</Group>
{/* Form */}
<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
type='number'
type="number"
value={apbDesaState.create.form.tahun}
onChange={(val) => {
apbDesaState.create.form.tahun = Number(val.target.value);
}}
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="masukkan tahun"
label={<Text fz="sm" fw="bold">Tahun</Text>}
placeholder="Masukkan tahun anggaran"
required
/>
<SelectPendapatan
selectedIds={apbDesaState.create.form.pendapatanIds}
onSelectionChange={(ids) => {
apbDesaState.create.form.pendapatanIds = ids;
}}
/>
<SelectBelanja
selectedIds={apbDesaState.create.form.belanjaIds}
onSelectionChange={(ids) => {
apbDesaState.create.form.belanjaIds = ids;
}}
/>
<SelectPembiayaan
selectedIds={apbDesaState.create.form.pembiayaanIds}
onSelectionChange={(ids) => {
apbDesaState.create.form.pembiayaanIds = ids;
}}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Action */}
<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>
);
/* Select Pendapatan */
/* ---------- Select Pendapatan ---------- */
interface SelectPendapatanProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectPendapatan({
selectedIds = [],
onSelectionChange,
}: SelectPendapatanProps) {
function SelectPendapatan({ selectedIds = [], onSelectionChange }: SelectPendapatanProps) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
useShallowEffect(() => {
pendapatanState.findMany.load().then(() => {
console.log("Pendapatan berhasil dimuat:", pendapatanState.findMany.data);
});
pendapatanState.findMany.load();
}, []);
if (!pendapatanState.findMany.data) {
@@ -93,10 +133,10 @@ function CreateAPBDesa() {
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pendapatan</Text>}
data={pendapatanState.findMany.data.map(p => ({
label={<Text fz="sm" fw="bold">Pendapatan</Text>}
data={pendapatanState.findMany.data.map((p) => ({
value: p.id,
label: p.name
label: p.name,
}))}
value={selectedIds}
onChange={onSelectionChange}
@@ -108,22 +148,16 @@ function CreateAPBDesa() {
);
}
/* Select Belanja */
/* ---------- Select Belanja ---------- */
interface SelectBelanjaProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectBelanja({
selectedIds = [],
onSelectionChange,
}: SelectBelanjaProps) {
function SelectBelanja({ selectedIds = [], onSelectionChange }: SelectBelanjaProps) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
useShallowEffect(() => {
belanjaState.findMany.load().then(() => {
console.log("Belanja berhasil dimuat:", belanjaState.findMany.data);
});
belanjaState.findMany.load();
}, []);
if (!belanjaState.findMany.data) {
@@ -132,10 +166,10 @@ function CreateAPBDesa() {
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Belanja</Text>}
data={belanjaState.findMany.data.map(b => ({
label={<Text fz="sm" fw="bold">Belanja</Text>}
data={belanjaState.findMany.data.map((b) => ({
value: b.id,
label: b.name
label: b.name,
}))}
value={selectedIds}
onChange={onSelectionChange}
@@ -147,22 +181,16 @@ function CreateAPBDesa() {
);
}
/* Select Pembiayaan */
/* ---------- Select Pembiayaan ---------- */
interface SelectPembiayaanProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectPembiayaan({
selectedIds = [],
onSelectionChange,
}: SelectPembiayaanProps) {
function SelectPembiayaan({ selectedIds = [], onSelectionChange }: SelectPembiayaanProps) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
useShallowEffect(() => {
pembiayaanState.findMany.load().then(() => {
console.log("Pembiayaan berhasil dimuat:", pembiayaanState.findMany.data);
});
pembiayaanState.findMany.load();
}, []);
if (!pembiayaanState.findMany.data) {
@@ -171,10 +199,10 @@ function CreateAPBDesa() {
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pembiayaan</Text>}
data={pembiayaanState.findMany.data.map(b => ({
label={<Text fz="sm" fw="bold">Pembiayaan</Text>}
data={pembiayaanState.findMany.data.map((b) => ({
value: b.id,
label: b.name
label: b.name,
}))}
value={selectedIds}
onChange={onSelectionChange}

View File

@@ -1,23 +1,38 @@
'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,
Paper,
Pagination,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
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 PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
function APBDesa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='APB Desa'
placeholder='pencarian'
title="APB Desa"
placeholder="Cari tahun atau nominal..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,77 +43,147 @@ function APBDesa() {
}
function ListAPBDesa({ search }: { search: string }) {
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter();
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
const {
data,
page,
totalPages,
loading,
load,
} = apbDesaState.findMany;
const formatRupiah = (value: number) =>
new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(value);
};
useShallowEffect(() => {
apbDesaState.findMany.load();
}, [])
load(page, 10, search);
}, [page, search]);
const filteredData = (apbDesaState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.tahun.toString().toLowerCase().includes(keyword) ||
item.pembiayaan.map((item) => item.value.toString()).includes(keyword) ||
item.belanja.map((item) => item.value.toString()).includes(keyword) ||
item.pendapatan.map((item) => item.value.toString()).includes(keyword)
);
});
const filteredData = data || [];
if (!apbDesaState.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List APB Desa'
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Tahun</TableTh>
<TableTh>Pembiayaan</TableTh>
<TableTh>Belanja</TableTh>
<TableTh>Pendapatan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.tahun}</TableTd>
<TableTd>{formatRupiah(item.pembiayaan.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd>
<TableTd>{formatRupiah(item.belanja.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd>
<TableTd>{formatRupiah(item.pendapatan.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd>
<TableTd>
<Button
bg={"green"}
onClick={() =>
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`)
}
>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
<Paper withBorder bg={colors["white-1"]} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Text fw={600} fz="lg">
List APB Desa
</Text>
<Tooltip label="Tambah APB Desa" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
"/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create"
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: "15%" }}>Tahun</TableTh>
<TableTh style={{ width: "25%" }}>Pembiayaan</TableTh>
<TableTh style={{ width: "25%" }}>Belanja</TableTh>
<TableTh style={{ width: "25%" }}>Pendapatan</TableTh>
<TableTh style={{ width: "10%" }}>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.tahun}</TableTd>
<TableTd>
{formatRupiah(
item.pembiayaan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</TableTd>
<TableTd>
{formatRupiah(
item.belanja.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</TableTd>
<TableTd>
{formatRupiah(
item.pendapatan.reduce(
(sum, val) => sum + Number(val.value),
0
)
)}
</TableTd>
<TableTd>
<Tooltip label="Lihat Detail" withArrow>
<Button
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data APB Desa yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<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>
</Box>
);
}

View File

@@ -2,7 +2,16 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
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';
@@ -72,41 +81,72 @@ function EditBelanja() {
}
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">
{/* Header */}
<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 Jenis Belanja
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jenis Pendapatan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>}
placeholder='Masukkan nama Jenis Pendapatan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
value={formatRupiah(formData.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
{/* Card */}
<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 Jenis Belanja"
placeholder="Masukkan nama jenis belanja"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Nilai"
placeholder="Masukkan nilai"
value={formatRupiah(formData.value)}
onChange={(e) => {
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
required
/>
<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 EditBelanja;
export default EditBelanja;

View File

@@ -1,17 +1,30 @@
'use client'
'use client';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
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';
import { toast } from 'react-toastify';
function CreateBelanja() {
const belanjaState = useProxy(PendapatanAsliDesa.belanja)
const router = useRouter()
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter();
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
const number =
typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -25,48 +38,83 @@ function CreateBelanja() {
const resetForm = () => {
belanjaState.create.form = {
name: "",
name: '',
value: 0,
}
}
};
};
const handleSubmit = async () => {
await belanjaState.create.submit();
resetForm()
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
if (!belanjaState.create.form.name || !belanjaState.create.form.value) {
return toast.warn('Lengkapi semua field terlebih dahulu');
}
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Belanja</Title>
await belanjaState.create.submit();
resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja');
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */}
<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 Jenis Belanja
</Title>
</Group>
{/* Card Form */}
<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 fw="bold" fz="sm">Nama Jenis Belanja</Text>}
placeholder="Masukkan nama jenis belanja"
value={belanjaState.create.form.name}
onChange={(val) => {
belanjaState.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Belanja</Text>}
placeholder='Masukkan nama jenis belanja'
onChange={(e) => (belanjaState.create.form.name = e.target.value)}
required
/>
<TextInput
type='text'
type="text"
label={<Text fw="bold" fz="sm">Nilai</Text>}
placeholder="Masukkan nilai belanja"
value={formatRupiah(belanjaState.create.form.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
belanjaState.create.form.value = cleanValue;
onChange={(e) => {
const raw = e.currentTarget.value;
belanjaState.create.form.value = unformatRupiah(raw);
}}
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
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>

View File

@@ -1,24 +1,41 @@
'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,
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, IconTrash } 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 { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
function Belanja() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Belanja'
placeholder='pencarian'
title="Belanja"
placeholder="Cari belanja berdasarkan nama atau nilai..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,108 +46,175 @@ function Belanja() {
}
function ListBelanja({ search }: { search: string }) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja)
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
loading,
load,
page,
totalPages,
} = belanjaState.findMany;
const formatRupiah = (value: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
const totalBelanja = belanjaState.findMany.data.reduce((sum, item) => sum + item.value, 0);
const totalBelanja = data?.reduce((sum, item) => sum + item.value, 0) || 0;
const handleDelete = () => {
if (selectedId) {
belanjaState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
belanjaState.findMany.load()
belanjaState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
load(page, 10, search);
}
}
};
useShallowEffect(() => {
belanjaState.findMany.load();
}, [])
load(page, 10, search);
}, [page, search]);
const filteredData = (belanjaState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.value.toString().toLowerCase().includes(keyword)
);
});
const filteredData = data || [];
if (!belanjaState.findMany.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 Belanja'
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Nilai</TableTh>
<TableTh>Persentase</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>{((item.value / totalBelanja) * 100).toFixed(0)}%</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={belanjaState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Belanja</Title>
<Tooltip label="Tambah Belanja" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Nilai</TableTh>
<TableTh>Persentase</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
))}
<TableTr>
<TableTd colSpan={4}>
<Text fw={'bold'}>Total</Text>
</TableTd>
<TableTd>
{formatRupiah(belanjaState.findMany.data.reduce((total, item) => total + item.value, 0))}
</TableTd>
</TableTr>
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
<>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>
{totalBelanja > 0
? ((item.value / totalBelanja) * 100).toFixed(0) + '%'
: '0%'}
</TableTd>
<TableTd>
<Group gap="xs">
<Tooltip label="Edit" withArrow>
<Button
size="xs"
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
</Tooltip>
<Tooltip label="Hapus" withArrow>
<Button
size="xs"
variant="light"
color="red"
disabled={belanjaState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Tooltip>
</Group>
</TableTd>
</TableTr>
))}
<TableTr>
<TableTd colSpan={2}>
<Text fw="bold">Total</Text>
</TableTd>
<TableTd colSpan={2}>
<Text fw="bold">{formatRupiah(totalBelanja)}</Text>
</TableTd>
</TableTr>
</>
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data belanja yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<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>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus belanja ini?'
text="Apakah anda yakin ingin menghapus belanja ini?"
/>
</Box>
);

View File

@@ -2,7 +2,16 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
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';
@@ -20,7 +29,7 @@ function EditPembiayaan() {
});
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -46,8 +55,8 @@ function EditPembiayaan() {
});
}
} catch (error) {
console.error("Error loading pembiayaan:", error);
toast.error("Gagal memuat data pembiayaan");
console.error('Error loading pembiayaan:', error);
toast.error('Gagal memuat data pembiayaan');
}
};
@@ -60,53 +69,84 @@ function EditPembiayaan() {
...pembiayaanState.update.form,
name: formData.name,
value: Number(formData.value),
}
};
await pembiayaanState.update.update();
toast.success("Jenis Pembiayaan berhasil diperbarui!");
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan");
toast.success('Jenis Pembiayaan berhasil diperbarui!');
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan');
} catch (error) {
console.error("Error updating jenis pembiayaan:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis pembiayaan");
console.error('Error updating jenis pembiayaan:', error);
toast.error('Terjadi kesalahan saat memperbarui jenis pembiayaan');
}
}
};
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">
{/* Header dengan Back Button */}
<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 Jenis Pembiayaan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jenis Pembiayaan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pembiayaan</Text>}
placeholder='Masukkan nama Jenis Pembiayaan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
value={formatRupiah(formData.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
{/* Card Form */}
<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 Jenis Pembiayaan"
placeholder="Masukkan nama jenis pembiayaan"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Nilai"
placeholder="Masukkan nilai"
value={formatRupiah(formData.value)}
onChange={(e) => {
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
required
/>
<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 EditPembiayaan;
export default EditPembiayaan;

View File

@@ -1,18 +1,30 @@
'use client'
'use client';
import React from 'react';
import { useProxy } from 'valtio/utils';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import { useRouter } from 'next/navigation';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Title,
TextInput,
Text,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { toast } from 'react-toastify';
function CreatePembiayaan() {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan)
const router = useRouter()
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
const router = useRouter();
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
const number =
typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -26,48 +38,85 @@ function CreatePembiayaan() {
const resetForm = () => {
pembiayaanState.create.form = {
name: "",
name: '',
value: 0,
}
}
};
};
const handleSubmit = async () => {
await pembiayaanState.create.submit();
resetForm()
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) {
return toast.warn('Nama dan nilai wajib diisi');
}
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Pembiayaan</Title>
await pembiayaanState.create.submit();
resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan');
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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 Jenis Pembiayaan
</Title>
</Group>
{/* Form Card */}
<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 fw="bold" fz="sm">Nama Jenis Pembiayaan</Text>}
placeholder="Masukkan nama jenis pembiayaan"
value={pembiayaanState.create.form.name}
onChange={(val) => {
pembiayaanState.create.form.name = val.target.value;
onChange={(e) => {
pembiayaanState.create.form.name = e.currentTarget.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pembiayaan</Text>}
placeholder='Masukkan nama jenis pembiayaan'
required
/>
<TextInput
type='text'
type="text"
label={<Text fw="bold" fz="sm">Nilai</Text>}
placeholder="Masukkan nilai"
value={formatRupiah(pembiayaanState.create.form.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
pembiayaanState.create.form.value = cleanValue;
onChange={(e) => {
const raw = e.currentTarget.value;
pembiayaanState.create.form.value = unformatRupiah(raw);
}}
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
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>

View File

@@ -1,14 +1,31 @@
'use client'
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
Pagination,
} from '@mantine/core';
import React, { useState } from 'react';
import HeaderSearch from '../../../_com/header';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import { useProxy } from 'valtio/utils';
import { useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks';
import colors from '@/con/colors';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function Pembiayaan() {
@@ -16,8 +33,8 @@ function Pembiayaan() {
return (
<Box>
<HeaderSearch
title='Pembiayaan'
placeholder='pencarian'
title="Pembiayaan"
placeholder="Cari nama pembiayaan..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,111 +45,171 @@ function Pembiayaan() {
}
function ListPembiayaan({ search }: { search: string }) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan)
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
const {
data,
page,
totalPages,
loading,
load,
} = pembiayaanState.findMany;
const formatRupiah = (value: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
const totalPembiayaan = pembiayaanState.findMany.data.reduce((sum, item) => sum + item.value, 0);
const totalPembiayaan = (data || []).reduce((sum, item) => sum + item.value, 0);
const handleDelete = () => {
if (selectedId) {
pembiayaanState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
pembiayaanState.findMany.load()
pembiayaanState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
load(page, 10, search);
}
}
};
useShallowEffect(() => {
pembiayaanState.findMany.load();
}, [])
load(page, 10, search);
}, [page, search]);
const filteredData = (pembiayaanState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.value.toString().toLowerCase().includes(keyword)
);
});
if (!pembiayaanState.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const filteredData = data || [];
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pembiayaan'
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Nilai</TableTh>
<TableTh>Persentase</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>{((item.value / totalPembiayaan) * 100).toFixed(0)}%</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={pembiayaanState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pembiayaan</Title>
<Tooltip label="Tambah Pembiayaan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Nilai</TableTh>
<TableTh>Persentase</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr>
))}
<TableTr>
<TableTd colSpan={4}>
<Text fw={'bold'}>Total</Text>
</TableTd>
<TableTd>
{formatRupiah(pembiayaanState.findMany.data.reduce((total, item) => total + item.value, 0))}
</TableTd>
</TableTr>
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
<>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>
{totalPembiayaan > 0
? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%'
: '0%'}
</TableTd>
<TableTd>
<Group gap="xs">
<Button
color="green"
variant="light"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/${item.id}`
)
}
>
<IconEdit size={18} />
</Button>
<Button
color="red"
variant="light"
disabled={pembiayaanState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Group>
</TableTd>
</TableTr>
))}
{/* Total Row */}
<TableTr>
<TableTd colSpan={2}>
<Text fw="bold">Total</Text>
</TableTd>
<TableTd colSpan={2}>{formatRupiah(totalPembiayaan)}</TableTd>
</TableTr>
</>
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data pembiayaan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<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>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus pembiayaan ini?'
text="Apakah anda yakin ingin menghapus pembiayaan ini?"
/>
</Box>
)
);
}
export default Pembiayaan;

View File

@@ -2,7 +2,16 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
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';
@@ -20,7 +29,7 @@ function EditPendapatan() {
});
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -28,15 +37,13 @@ function EditPendapatan() {
}).format(number);
};
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
};
const unformatRupiah = (value: string) => Number(value.replace(/\D/g, ''));
useEffect(() => {
const loadPendapatan = async () => {
const id = params?.id as string;
if (!id) return;
const id = params?.id as string;
if (!id) return;
const loadPendapatan = async () => {
try {
const data = await pendapatanState.update.load(id);
if (data) {
@@ -46,8 +53,8 @@ function EditPendapatan() {
});
}
} catch (error) {
console.error("Error loading pendapatan:", error);
toast.error("Gagal memuat data pendapatan");
console.error('Error loading pendapatan:', error);
toast.error('Gagal memuat data pendapatan');
}
};
@@ -60,53 +67,84 @@ function EditPendapatan() {
...pendapatanState.update.form,
name: formData.name,
value: Number(formData.value),
}
};
await pendapatanState.update.update();
toast.success("Jenis Pendapatan berhasil diperbarui!");
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan");
toast.success('Jenis Pendapatan berhasil diperbarui!');
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan');
} catch (error) {
console.error("Error updating jenis pendapatan:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis pendapatan");
console.error('Error updating jenis pendapatan:', error);
toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan');
}
}
};
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">
{/* Header with Back Button */}
<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 Jenis Pendapatan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jenis Pendapatan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>}
placeholder='Masukkan nama Jenis Pendapatan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
value={formatRupiah(formData.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
{/* Card Form */}
<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 Jenis Pendapatan"
placeholder="Masukkan nama jenis pendapatan"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Nilai"
placeholder="Masukkan nilai"
value={formatRupiah(formData.value)}
onChange={(e) => {
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
required
/>
<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 EditPendapatan;
export default EditPendapatan;

View File

@@ -1,14 +1,24 @@
'use client'
'use client';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
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';
function CreatePendapatan() {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan)
const router = useRouter()
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
const router = useRouter();
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
@@ -25,48 +35,90 @@ function CreatePendapatan() {
const resetForm = () => {
pendapatanState.create.form = {
name: "",
name: '',
value: 0,
}
}
};
};
const handleSubmit = async () => {
await pendapatanState.create.submit();
resetForm()
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan');
};
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Pendapatan</Title>
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol back + judul */}
<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 Jenis Pendapatan
</Title>
</Group>
{/* Card Form */}
<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
value={pendapatanState.create.form.name}
onChange={(val) => {
pendapatanState.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>}
placeholder='Masukkan nama jenis pendapatan'
label={
<Text fw="bold" fz="sm">
Nama Jenis Pendapatan
</Text>
}
placeholder="Masukkan nama jenis pendapatan"
required
/>
<TextInput
type='text'
type="text"
value={formatRupiah(pendapatanState.create.form.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
pendapatanState.create.form.value = cleanValue;
}}
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
label={
<Text fw="bold" fz="sm">
Nilai
</Text>
}
placeholder="Masukkan nilai"
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>

View File

@@ -1,16 +1,33 @@
'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,
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, IconTrash } 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 { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
function Pendapatan() {
const [search, setSearch] = useState("");
@@ -18,7 +35,7 @@ function Pendapatan() {
<Box>
<HeaderSearch
title='Pendapatan'
placeholder='pencarian'
placeholder='Cari pendapatan...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,105 +46,166 @@ function Pendapatan() {
}
function ListPendapatan({ search }: { search: string }) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan)
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
page,
totalPages,
loading,
load,
} = pendapatanState.findMany;
const formatRupiah = (value: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
const handleDelete = () => {
if (selectedId) {
pendapatanState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
pendapatanState.findMany.load()
pendapatanState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
load(page, 10, search);
}
}
};
useShallowEffect(() => {
pendapatanState.findMany.load();
}, [])
load(page, 10, search);
}, [page, search]);
const filteredData = (pendapatanState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.value.toString().toLowerCase().includes(keyword)
);
});
const filteredData = data || [];
if (!pendapatanState.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={600} radius="md" />
</Stack>
)
);
}
const totalValue = filteredData.reduce((total, item) => total + item.value, 0);
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pendapatan'
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Nilai</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={pendapatanState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Pendapatan</Title>
<Tooltip label="Tambah Pendapatan Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '40%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Nilai</TableTh>
<TableTh style={{ width: '15%' }}>Edit</TableTh>
<TableTh style={{ width: '15%' }}>Delete</TableTh>
</TableTr>
))}
<TableTr>
<TableTd colSpan={4}>
<Text fw={'bold'}>Total</Text>
</TableTd>
<TableTd>
{formatRupiah(pendapatanState.findMany.data.reduce((total, item) => total + item.value, 0))}
</TableTd>
</TableTr>
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
<>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)
}
>
<IconEdit size={18} />
<Text ml={5}>Edit</Text>
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={pendapatanState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
<Text ml={5}>Hapus</Text>
</Button>
</TableTd>
</TableTr>
))}
{/* Row total */}
<TableTr>
<TableTd colSpan={1}>
<Text fw={'bold'}>Total</Text>
</TableTd>
<TableTd colSpan={3}>
<Text fw={'bold'}>{formatRupiah(totalValue)}</Text>
</TableTd>
</TableTr>
</>
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data pendapatan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<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>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus pendapatan ini?'
text="Apakah anda yakin ingin menghapus pendapatan ini?"
/>
</Box>
);

View File

@@ -1,8 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'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';
@@ -11,33 +20,33 @@ import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function EditDemografiPekerjaan() {
const router = useRouter()
const params = useParams() as { id: string }
const stateDemografi = useProxy(demografiPekerjaan)
const router = useRouter();
const params = useParams() as { id: string };
const stateDemografi = useProxy(demografiPekerjaan);
const id = params.id
const id = params.id;
useEffect(() => {
if (!id) return;
stateDemografi.update.id = id;
stateDemografi.findUnique.load(id)
stateDemografi.findUnique
.load(id)
.then(() => {
const data = stateDemografi.findUnique.data;
if (data) {
stateDemografi.update.form = {
pekerjaan: String(data.pekerjaan || ''),
lakiLaki: Number(data.lakiLaki || 0),
perempuan: Number(data.perempuan || 0)
perempuan: Number(data.perempuan || 0),
};
}
})
.catch(error => {
.catch((error) => {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
});
}, [id]);
// Di handleSubmit, ubah menjadi:
const handleSubmit = async () => {
try {
stateDemografi.update.id = id;
@@ -51,52 +60,88 @@ function EditDemografiPekerjaan() {
};
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 Demografi Pekerjaan</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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 Demografi Pekerjaan
</Title>
</Group>
{/* Form Card */}
<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="Pekerjaan"
placeholder="masukkan pekerjaan"
placeholder="Masukkan jenis pekerjaan"
value={stateDemografi.update.form.pekerjaan}
onChange={(val) => {
stateDemografi.update.form.pekerjaan = val.currentTarget.value;
}}
onChange={(e) =>
(stateDemografi.update.form.pekerjaan = e.currentTarget.value)
}
required
/>
<TextInput
label="Jumlah Pekerja Laki - Laki"
label="Jumlah Pekerja Laki-laki"
type="number"
placeholder="masukkan jumlah pekerja laki - laki"
placeholder="Masukkan jumlah pekerja laki-laki"
value={stateDemografi.update.form.lakiLaki}
onChange={(val) => {
stateDemografi.update.form.lakiLaki = Number(val.currentTarget.value);
}}
onChange={(e) =>
(stateDemografi.update.form.lakiLaki = Number(
e.currentTarget.value
))
}
required
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
placeholder="masukkan jumlah pekerja perempuan"
placeholder="Masukkan jumlah pekerja perempuan"
value={stateDemografi.update.form.perempuan}
onChange={(val) => {
stateDemografi.update.form.perempuan = Number(val.currentTarget.value);
}}
onChange={(e) =>
(stateDemografi.update.form.perempuan = Number(
e.currentTarget.value
))
}
required
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan Perubahan
</Button>
<Group justify="flex-end">
<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 EditDemografiPekerjaan;

View File

@@ -1,8 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
'use client';
import colors from '@/con/colors';
import { Box, Button, Group, 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 { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -12,15 +22,15 @@ import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function CreateDemografiPekerjaan() {
const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter()
const router = useRouter();
const resetForm = () => {
stateDemografi.create.form = {
pekerjaan: "",
pekerjaan: '',
lakiLaki: 0,
perempuan: 0,
}
}
};
};
const handleSubmit = async () => {
const id = await stateDemografi.create.create();
@@ -32,58 +42,85 @@ function CreateDemografiPekerjaan() {
}
}
resetForm();
router.push("/admin/ekonomi/demografi-pekerjaan");
}
router.push('/admin/ekonomi/demografi-pekerjaan');
};
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 Demografi Pekerjaan</Title>
<Stack gap={"xs"}>
<TextInput
label="Pekerjaan"
type="text"
value={stateDemografi.create.form.pekerjaan}
placeholder="Masukkan pekerjaan"
onChange={(val) => {
stateDemografi.create.form.pekerjaan = val.currentTarget.value;
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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 Demografi Pekerjaan
</Title>
</Group>
{/* Form */}
<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="Pekerjaan"
type="text"
value={stateDemografi.create.form.pekerjaan}
placeholder="Masukkan pekerjaan"
onChange={(val) => {
stateDemografi.create.form.pekerjaan = val.currentTarget.value;
}}
required
/>
<TextInput
label="Jumlah Pekerja Laki-Laki"
type="number"
value={stateDemografi.create.form.lakiLaki}
placeholder="Masukkan jumlah pekerja laki-laki"
onChange={(val) => {
stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
}}
required
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
value={stateDemografi.create.form.perempuan}
placeholder="Masukkan jumlah pekerja perempuan"
onChange={(val) => {
stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
}}
required
/>
<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)',
}}
/>
<TextInput
label="Jumlah Pekerja Laki - Laki"
type="number"
value={stateDemografi.create.form.lakiLaki}
placeholder="Masukkan jumlah pekerja laki - laki"
onChange={(val) => {
stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
}}
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
value={stateDemografi.create.form.perempuan}
placeholder="Masukkan jumlah pekerja perempuan"
onChange={(val) => {
stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,14 +1,32 @@
'use client'
import colors from '@/con/colors';
import { BarChart } from '@mantine/charts';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
Pagination,
Flex,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { IconEdit, IconPlus, IconSearch, IconTrash } 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 { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import demografiPekerjaan from '../../_state/ekonomi/demografi-pekerjaan';
@@ -18,7 +36,7 @@ function DemografiPekerjaan() {
<Box>
<HeaderSearch
title='Demografi Pekerjaan'
placeholder='pencarian'
placeholder='Cari pekerjaan atau jumlah pekerja...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -34,131 +52,193 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
pekerjaan: string;
lakiLaki: number;
perempuan: number;
}
};
const router = useRouter();
const stateDemografi = useProxy(demografiPekerjaan)
const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
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 {
data,
page,
totalPages,
loading,
load,
} = stateDemografi.findMany;
const handleDelete = () => {
if (selectedId) {
stateDemografi.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateDemografi.findMany.load()
stateDemografi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
}
};
useShallowEffect(() => {
setMounted(true)
stateDemografi.findMany.load()
}, [])
setMounted(true);
load(page, 10, search);
}, [page, search]);
useEffect(() => {
setMounted(true);
if (stateDemografi.findMany.data) {
setChartData(stateDemografi.findMany.data.map((item) => ({
id: item.id,
pekerjaan: item.pekerjaan,
lakiLaki: Number(item.lakiLaki),
perempuan: Number(item.perempuan),
})));
if (data) {
setChartData(
data.map((item) => ({
id: item.id,
pekerjaan: item.pekerjaan,
lakiLaki: Number(item.lakiLaki),
perempuan: Number(item.perempuan),
}))
);
}
}, [stateDemografi.findMany.data]);
}, [data]);
const filteredData = (stateDemografi.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
const filteredData = data || [];
if (loading || !data) {
return (
item.pekerjaan.toLowerCase().includes(keyword) ||
item.lakiLaki.toString().toLowerCase().includes(keyword) ||
item.perempuan.toString().toLowerCase().includes(keyword)
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
});
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Demografi Pekerjaan'
href='/admin/ekonomi/demografi-pekerjaan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Pekerjaan</TableTh>
<TableTh>Jumlah Pekerja Laki - Laki</TableTh>
<TableTh>Jumlah Pekerja Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.pekerjaan}</TableTd>
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stateDemografi.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Demografi Pekerjaan</Title>
<Tooltip label="Tambah Data Pekerjaan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Pekerjaan</TableTh>
<TableTh>Laki - Laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.pekerjaan}</TableTd>
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)
}
>
<IconEdit size={18} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={stateDemografi.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data demografi pekerjaan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<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>
{/* 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}>Data Kelahiran & Kematian</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}>Data Kelahiran & Kematian</Title>
{mounted && chartData.length > 0 && (
<Box w={{ base: '100%', md: '30%' }}>
<BarChart
h={450}
data={chartData}
dataKey="pekerjaan"
type="stacked"
series={[
{ name: 'lakiLaki', color: '#5082EE', label: 'Laki - Laki' },
{ name: 'perempuan', color: '#6EDF9C', label: 'Perempuan' },
]}
/>
</Box>
<Box mt={30} style={{ width: '100%', minHeight: 400 }}>
<Paper bg={colors['white-1']} p="md" radius="md" withBorder>
<Stack gap={"xs"}>
<Title pb={10} order={4}>
Grafik Demografi Pekerjaan
</Title>
{mounted && chartData.length > 0 ? (
<BarChart
h={450}
data={chartData}
dataKey="pekerjaan"
type="stacked"
series={[
{ name: 'lakiLaki', color: '#5082EE', label: 'Laki - Laki' },
{ name: 'perempuan', color: '#6EDF9C', label: 'Perempuan' },
]}
/>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Paper>
</Box>
)}
<Box py={10}>
<Group justify='center'>
<Flex align="center" gap={10}>
<Box bg="#5082EE" w={20} h={20} />
<Text>Laki - Laki</Text>
</Flex>
<Flex align="center" gap={10}>
<Box bg="#6EDF9C" w={20} h={20} />
<Text>Perempuan</Text>
</Flex>
</Group>
</Box>
</Stack>
</Paper>
</Box>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus demografi pekerjaan ini?'
text="Apakah anda yakin ingin menghapus demografi pekerjaan ini?"
/>
</Box>
);

View File

@@ -3,78 +3,116 @@
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { Box, Button, Paper, Stack, TextInput, Title, Group, 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 { toast } from 'react-toastify';
function EditJumlahPendudukMiskin() {
const router = useRouter()
const params = useParams() as { id: string }
const stateJPM = useProxy(jumlahPendudukMiskin)
const router = useRouter();
const params = useParams() as { id: string };
const stateJPM = useProxy(jumlahPendudukMiskin);
const id = params.id
const id = params.id;
// Load data saat komponen mount
useEffect(() => {
if (id) {
stateJPM.findUnique.load(id).then(() => {
const data = stateJPM.findUnique.data
if (!id) return;
const loadData = async () => {
try {
await stateJPM.findUnique.load(id);
const data = stateJPM.findUnique.data;
if (data) {
stateJPM.update.form = {
year: data.year || 0,
totalPoorPopulation: data.totalPoorPopulation || 0,
}
};
}
})
}
}, [id])
} catch (error) {
console.error('Gagal memuat data:', error);
toast.error('Gagal memuat data jumlah penduduk miskin');
}
};
loadData();
}, [id]);
const handleSubmit = async () => {
// Set the ID before submitting
stateJPM.update.id = id;
await stateJPM.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-miskin')
}
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 Jumlah Penduduk Miskin</Title>
try {
stateJPM.update.id = id;
await stateJPM.update.submit();
toast.success('Data jumlah penduduk miskin berhasil diperbarui!');
router.push('/admin/ekonomi/jumlah-penduduk-miskin');
} catch (error) {
console.error('Gagal menyimpan data:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
}
};
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 Jumlah Penduduk Miskin
</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="Tahun"
placeholder="masukkan tahun"
placeholder="Masukkan tahun"
type="number"
required
value={stateJPM.update.form.year}
onChange={(val) => {
stateJPM.update.form.year = Number(val.currentTarget.value);
}}
/>
<TextInput
label="Jumlah Penduduk Miskin"
placeholder="Masukkan jumlah penduduk miskin"
type="number"
placeholder="masukkan jumlah penduduk miskin"
required
value={stateJPM.update.form.totalPoorPopulation}
onChange={(val) => {
stateJPM.update.form.totalPoorPopulation = Number(val.currentTarget.value);
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan
</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 EditJumlahPendudukMiskin;

View File

@@ -1,26 +1,25 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
'use client';
import { Box, Button, Group, Paper, Stack, 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 colors from '@/con/colors';
import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin';
function CreateJumlahPendudukMiskin() {
export default function CreateJumlahPendudukMiskin() {
const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter()
const router = useRouter();
const resetForm = () => {
stateJPM.create.form = {
year: 0,
year: new Date().getFullYear(),
totalPoorPopulation: 0,
}
}
};
};
const handleSubmit = async () => {
const id = await stateJPM.create.create();
@@ -32,51 +31,72 @@ function CreateJumlahPendudukMiskin() {
}
}
resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-miskin");
}
router.push('/admin/ekonomi/jumlah-penduduk-miskin');
};
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 Grafik Hasil Kepuasan Masyarakat</Title>
<Stack gap={"xs"}>
<TextInput
label="Tahun"
type="number"
value={stateJPM.create.form.year}
placeholder="Masukkan tahun"
onChange={(val) => {
stateJPM.create.form.year = Number(val.currentTarget.value);
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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 Jumlah Penduduk Miskin
</Title>
</Group>
{/* Form Paper */}
<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="Tahun"
type="number"
value={stateJPM.create.form.year || ''}
placeholder="Masukkan tahun"
onChange={(e) => {
const value = e.currentTarget.value;
stateJPM.create.form.year = value ? Number(value) : 0;
}}
required
/>
<TextInput
label="Jumlah Penduduk Miskin"
type="number"
value={stateJPM.create.form.totalPoorPopulation}
placeholder="Masukkan jumlah penduduk miskin"
onChange={(e) => {
stateJPM.create.form.totalPoorPopulation = Number(e.currentTarget.value);
}}
required
/>
<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)',
}}
/>
<TextInput
label="Jumlah Penduduk Miskin"
type="number"
value={stateJPM.create.form.totalPoorPopulation}
placeholder="Masukkan jumlah penduduk miskin"
onChange={(val) => {
stateJPM.create.form.totalPoorPopulation = Number(val.currentTarget.value);
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateJumlahPendudukMiskin;

View File

@@ -1,15 +1,14 @@
'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, Tooltip } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { useShallowEffect, useMediaQuery } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip } from 'recharts';
import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip as RechartsTooltip } from 'recharts';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
function JumlahPendudukMiskin() {
@@ -18,7 +17,7 @@ function JumlahPendudukMiskin() {
<Box>
<HeaderSearch
title='Jumlah Penduduk Miskin'
placeholder='pencarian'
placeholder='Cari tahun atau jumlah penduduk miskin...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,138 +28,158 @@ function JumlahPendudukMiskin() {
}
function ListJumlahPendudukMiskin({ search }: { search: string }) {
type JPMGrafik = {
id: string;
year: number;
totalPoorPopulation: number;
}
type JPMGrafik = { id: string; year: number; totalPoorPopulation: number }
const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<JPMGrafik[]>([]);
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) {
stateJPM.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateJPM.findMany.load()
}
}
const isTablet = useMediaQuery('(max-width:1024px)');
const isMobile = useMediaQuery('(max-width:768px)');
const {
data,
page,
loading,
load,
totalPages,
} = stateJPM.findMany;
// Load data
useShallowEffect(() => {
setMounted(true)
stateJPM.findMany.load()
}, [])
setMounted(true);
load(page, 10, search);
}, [page, search]);
useEffect(() => {
setMounted(true);
if (stateJPM.findMany.data) {
setChartData(stateJPM.findMany.data.map((item) => ({
setChartData(stateJPM.findMany.data.map(item => ({
id: item.id,
year: Number(item.year),
totalPoorPopulation: Number(item.totalPoorPopulation),
totalPoorPopulation: Number(item.totalPoorPopulation)
})));
}
}, [stateJPM.findMany.data]);
const filteredData = (stateJPM.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.year.toString().toLowerCase().includes(keyword) ||
item.totalPoorPopulation.toString().toLowerCase().includes(keyword)
);
});
const filteredData = data || []
if (!stateJPM.findMany.data) {
const handleDelete = () => {
if (selectedId) {
stateJPM.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
stateJPM.findMany.load();
}
}
if (loading || !data) {
return (
<Stack>
<Skeleton h={500} />
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Stack gap={'xs'}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jumlah Penduduk Miskin'
href='/admin/ekonomi/jumlah-penduduk-miskin/create'
/>
<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 Jumlah Penduduk Miskin</Title>
<Tooltip label="Tambah Data" withArrow>
<Button
leftSection={<IconEdit size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-miskin/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Tahun</TableTh>
<TableTh>Jumlah Penduduk Miskin</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
<TableTh style={{ width: '25%' }}>Tahun</TableTh>
<TableTh style={{ width: '35%' }}>Jumlah Penduduk Miskin</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
{filteredData.length > 0 ? filteredData.map(item => (
<TableTr key={item.id}>
<TableTd>{item.year}</TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}>
<Button variant='light' color="green" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stateJPM.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<Button variant='light' color="red" disabled={stateJPM.delete.loading} onClick={() => { setSelectedId(item.id); setModalHapus(true) }}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
)) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Paper>
</Box>
</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 Jumlah Penduduk Miskin</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 Jumlah Penduduk Miskin</Title>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
<XAxis dataKey="year" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="totalPoorPopulation" fill={colors['blue-button']} name="Jumlah Penduduk Miskin" />
</BarChart>
)}
</Paper>
</Box>
)}
</Stack>
<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 */}
{/* Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack>
<Box mt="lg" style={{ width: '100%', minHeight: 350 }}>
<Title order={4} mb="sm">Grafik Jumlah Penduduk Miskin</Title>
{mounted && chartData.length > 0 ? (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData}>
<XAxis dataKey="year" />
<YAxis />
<RechartsTooltip />
<Legend />
<Bar dataKey="totalPoorPopulation" fill={colors['blue-button']} name="Jumlah Penduduk Miskin" />
</BarChart>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Box>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik jumlah penduduk miskin ini?'
text='Apakah anda yakin ingin menghapus data ini?'
/>
</Box>
);

View File

@@ -1,62 +1,124 @@
/* 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,
ScrollArea,
} from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconUsers, IconSchool } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Pengangguran Berdasarkan Usia",
value: "pengangguranberdasarkanusia",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia"
},
{
label: "Pengangguran Berdasarkan Pendidikan",
value: "pengangguranberdasarkanpendidikan",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan"
},
];
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: "Pengangguran Berdasarkan Usia",
value: "pengangguranberdasarkanusia",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Data pengangguran menurut kelompok usia",
},
{
label: "Pengangguran Berdasarkan Pendidikan",
value: "pengangguranberdasarkanpendidikan",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan",
icon: <IconSchool size={18} stroke={1.8} />,
tooltip: "Data pengangguran menurut tingkat pendidikan",
},
];
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
return (
<Stack>
<Title order={3}>Jumlah Penduduk Usia Kerja yang Menganggur</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>
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 gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Jumlah Penduduk Usia Kerja yang Menganggur
</Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{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",
flexShrink: 0,
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{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;
export default LayoutTabs;

View File

@@ -1,108 +1,117 @@
'use client'
'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
/* eslint-disable react-hooks/exhaustive-deps */
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';
function EditGrafikBerdasarkanPendidikan() {
const router = useRouter()
const params = useParams() as { id: string }
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan)
const id = params.id
const router = useRouter();
const params = useParams() as { id: string };
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const id = params.id;
useEffect(() => {
if(id){
if (id) {
stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data
if(data){
const data = stategrafik.findUnique.data;
if (data) {
stategrafik.update.form = {
SD: data.SD || '',
SMP: data.SMP || '',
SMA: data.SMA || '',
D3: data.D3 || '',
S1: data.S1 || '',
}
};
}
})
});
}
}, [id])
}, [id]);
const handleSubmit = async () => {
stategrafik.update.id = id;
await stategrafik.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan')
}
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan');
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
<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 Grafik Pengangguran Berdasarkan Pendidikan
</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="SD"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.SD}
onChange={(val) => (stategrafik.update.form.SD = val.currentTarget.value)}
/>
<TextInput
label="SMP"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.SMP}
onChange={(val) => (stategrafik.update.form.SMP = val.currentTarget.value)}
/>
<TextInput
label="SMA"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.SMA}
onChange={(val) => (stategrafik.update.form.SMA = val.currentTarget.value)}
/>
<TextInput
label="D3"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.D3}
onChange={(val) => (stategrafik.update.form.D3 = val.currentTarget.value)}
/>
<TextInput
label="S1"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.S1}
onChange={(val) => (stategrafik.update.form.S1 = val.currentTarget.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>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Grafik Pengangguran Berdasarkan Pendidikan</Title>
<TextInput
label="SD"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.update.form.SD}
onChange={(val) => {
stategrafik.update.form.SD = val.currentTarget.value;
}}
/>
<TextInput
label="SMP"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.SMP}
onChange={(val) => {
stategrafik.update.form.SMP = val.currentTarget.value;
}}
/>
<TextInput
label="SMA"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.SMA}
onChange={(val) => {
stategrafik.update.form.SMA = val.currentTarget.value;
}}
/>
<TextInput
label="D3"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.D3}
onChange={(val) => {
stategrafik.update.form.D3 = val.currentTarget.value;
}}
/>
<TextInput
label="S1"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.S1}
onChange={(val) => {
stategrafik.update.form.S1 = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,32 +1,30 @@
'use client'
'use client';
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
function CreateGrafikBerdasarkanPendidikan() {
const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan)
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => {
stategrafik.create.form = {
...stategrafik.create.form,
SD: "",
SMP: "",
SMA: "",
D3: "",
S1: "",
}
}
SD: '',
SMP: '',
SMA: '',
D3: '',
S1: '',
};
};
const handleSubmit = async () => {
const id = await stategrafik.create.create();
@@ -38,73 +36,91 @@ function CreateGrafikBerdasarkanPendidikan() {
}
}
resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan")
}
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_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}>Create Grafik Pengangguran Berdasarkan Pendidikan</Title>
<TextInput
label="SD"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.create.form.SD}
onChange={(val) => {
stategrafik.create.form.SD = val.currentTarget.value;
}}
/>
<TextInput
label="SMP"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.SMP}
onChange={(val) => {
stategrafik.create.form.SMP = val.currentTarget.value;
}}
/>
<TextInput
label="SMA"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.SMA}
onChange={(val) => {
stategrafik.create.form.SMA = val.currentTarget.value;
}}
/>
<TextInput
label="D3"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.D3}
onChange={(val) => {
stategrafik.create.form.D3 = val.currentTarget.value;
}}
/>
<TextInput
label="S1"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.S1}
onChange={(val) => {
stategrafik.create.form.S1 = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</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 Data Pengangguran Berdasarkan Pendidikan
</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="SD"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.create.form.SD}
onChange={(val) => (stategrafik.create.form.SD = val.currentTarget.value)}
required
/>
<TextInput
label="SMP"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.create.form.SMP}
onChange={(val) => (stategrafik.create.form.SMP = val.currentTarget.value)}
required
/>
<TextInput
label="SMA"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.create.form.SMA}
onChange={(val) => (stategrafik.create.form.SMA = val.currentTarget.value)}
required
/>
<TextInput
label="D3"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.create.form.D3}
onChange={(val) => (stategrafik.create.form.D3 = val.currentTarget.value)}
required
/>
<TextInput
label="S1"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.create.form.S1}
onChange={(val) => (stategrafik.create.form.S1 = val.currentTarget.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)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,59 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanPendidikan() {
const [search, setSearch] = useState("")
const [search, setSearch] = useState("");
return (
<Box>
<Stack gap={"xs"}>
<HeaderSearch
title='Detail Data Pengangguran Berdasarkan Pendidikan'
placeholder='pencarian'
placeholder='Cari data pendidikan...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGrafikBerdasarkanPendidikan search={search}/>
</Stack>
<ListGrafikBerdasarkanPendidikan search={search} />
</Box>
);
}
function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan)
function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const handleDelete = async () => {
if (selectedId) {
await stategrafik.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stategrafik.findMany.load()
await stategrafik.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
stategrafik.findMany.load();
}
}
};
const {
data,
page,
totalPages,
loading,
load,
} = stategrafik.findMany;
useShallowEffect(() => {
setMounted(true);
stategrafik.findMany.load()
}, []);
load(page, 10, search);
}, [page, search]);
useEffect(() => {
if (stategrafik.findMany.data) {
@@ -70,36 +73,37 @@ function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
{ name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' },
]);
}
}, [stategrafik.findMany.data])
}, [stategrafik.findMany.data]);
const filteredData = (stategrafik.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
const filteredData = data || []
if (loading || !data) {
return (
item.SD.toString().toLowerCase().includes(keyword) ||
item.SMP.toString().toLowerCase().includes(keyword) ||
item.SMA.toString().toLowerCase().includes(keyword) ||
item.D3.toString().toLowerCase().includes(keyword) ||
item.S1.toString().toLowerCase().includes(keyword)
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
});
if (!stategrafik.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}>
<JudulList
title='List Grafik Pengangguran Berdasarkan Pendidikan'
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create'
/>
<Table striped withTableBorder withRowBorders>
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
{/* Header */}
<Flex justify="space-between" align="center" mb="md">
<Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
<Tooltip label="Tambah Data" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Flex>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>SD</TableTh>
@@ -114,8 +118,10 @@ function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={6}>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text>
<TableTd colSpan={7}>
<Center py={20}>
<Text color="dimmed">Belum ada data grafik responden</Text>
</Center>
</TableTd>
</TableTr>
) : (
@@ -127,82 +133,87 @@ function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
<TableTd>{item.D3}</TableTd>
<TableTd>{item.S1}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`)}>
<IconEdit size={20} />
</Button>
<Tooltip label="Edit Data" withArrow>
<Button color="green" variant="light" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`)}>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
<Tooltip label="Hapus Data" withArrow>
<Button
color="red"
variant="light"
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Paper>
</Box>
</Paper>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Pengangguran Berdasarkan Pendidikan</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}
data={donutData}
>
<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>
{/* Chart */}
<Box mt="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack>
<Title order={3} pb={10}>Grafik Pengangguran Berdasarkan Pendidikan</Title>
{mounted && donutData.length > 0 ? (
<Box style={{ width: '100%', minHeight: 250 }}>
<PieChart width={800} height={300} data={donutData}>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
cx={500}
cx={400}
cy={150}
innerRadius={60}
outerRadius={115}
label={true}
label
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>SD : {donutData.find((entry) => entry.name === 'SD')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} />
<Text>SMP : {donutData.find((entry) => entry.name === 'SMP')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#C07B13FF'} w={20} h={20} />
<Text>SMA : {donutData.find((entry) => entry.name === 'SMA')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#1094A8FF'} w={20} h={20} />
<Text>D3 : {donutData.find((entry) => entry.name === 'D3')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#A83610FF'} w={20} h={20} />
<Text>S1 : {donutData.find((entry) => entry.name === 'S1')?.value}</Text>
</Flex>
<Stack gap="xs" mt="sm">
{donutData.map((entry) => (
<Flex key={entry.key} gap="sm" align="center">
<Box w={20} h={20} bg={entry.color} />
<Text>{entry.name} : {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
</Box>
</Stack>
) : (
<Text color="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
</Box>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus

View File

@@ -1,98 +1,130 @@
'use client'
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { Box, Button, Paper, Stack, TextInput, Title, Group, 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 { toast } from 'react-toastify';
function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter()
const params = useParams() as { id: string }
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur)
const id = params.id
const router = useRouter();
const params = useParams() as { id: string };
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const id = params.id;
useEffect(() => {
if(id){
if (id) {
stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data
if(data){
const data = stategrafik.findUnique.data;
if (data) {
stategrafik.update.form = {
usia18_25: data.usia18_25 || '',
usia26_35: data.usia26_35 || '',
usia36_45: data.usia36_45 || '',
usia46_keatas: data.usia46_keatas || '',
}
};
}
})
});
}
}, [id])
}, [id]);
const handleSubmit = async () => {
stategrafik.update.id = id;
await stategrafik.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia')
}
try {
stategrafik.update.id = id;
await stategrafik.update.submit();
toast.success('Data grafik berhasil diperbarui!');
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'
);
} catch (error) {
console.error(error);
toast.error('Terjadi kesalahan saat memperbarui data grafik');
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
<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 Grafik Pengangguran Berdasarkan Usia Kerja
</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="Usia 18 - 25"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia18_25}
onChange={(val) => {
stategrafik.update.form.usia18_25 = val.currentTarget.value;
}}
required
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia26_35}
onChange={(val) => {
stategrafik.update.form.usia26_35 = val.currentTarget.value;
}}
required
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia36_45}
onChange={(val) => {
stategrafik.update.form.usia36_45 = val.currentTarget.value;
}}
required
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia46_keatas}
onChange={(val) => {
stategrafik.update.form.usia46_keatas = val.currentTarget.value;
}}
required
/>
<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>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Grafik Pengangguran Berdasarkan Usia Kerja</Title>
<TextInput
label="Usia 18 - 25"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia18_25}
onChange={(val) => {
stategrafik.update.form.usia18_25 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia26_35}
onChange={(val) => {
stategrafik.update.form.usia26_35 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia36_45}
onChange={(val) => {
stategrafik.update.form.usia36_45 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia46_keatas}
onChange={(val) => {
stategrafik.update.form.usia46_keatas = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,31 +1,29 @@
'use client'
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur)
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => {
stategrafik.create.form = {
...stategrafik.create.form,
usia18_25: "",
usia26_35: "",
usia36_45: "",
usia46_keatas: "",
}
}
usia18_25: '',
usia26_35: '',
usia36_45: '',
usia46_keatas: '',
};
};
const handleSubmit = async () => {
const id = await stategrafik.create.create();
@@ -37,64 +35,84 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
}
}
resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia")
}
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia');
};
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}>Create Grafik Pengangguran Berdasarkan Usia Kerja</Title>
<TextInput
label="Usia 18 - 25"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.create.form.usia18_25}
onChange={(val) => {
stategrafik.create.form.usia18_25 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.usia26_35}
onChange={(val) => {
stategrafik.create.form.usia26_35 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.usia36_45}
onChange={(val) => {
stategrafik.create.form.usia36_45 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.create.form.usia46_keatas}
onChange={(val) => {
stategrafik.create.form.usia46_keatas = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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 Data Pengangguran Berdasarkan Usia
</Title>
</Group>
{/* Form Paper */}
<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="Usia 18 - 25"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.create.form.usia18_25}
onChange={(val) => (stategrafik.create.form.usia18_25 = val.currentTarget.value)}
required
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.create.form.usia26_35}
onChange={(val) => (stategrafik.create.form.usia26_35 = val.currentTarget.value)}
required
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.create.form.usia36_45}
onChange={(val) => (stategrafik.create.form.usia36_45 = val.currentTarget.value)}
required
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.create.form.usia46_keatas}
onChange={(val) => (stategrafik.create.form.usia46_keatas = val.currentTarget.value)}
required
/>
{/* 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>
);
}

View File

@@ -1,60 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
const [search, setSearch] = useState("")
const [search, setSearch] = useState('');
return (
<Box>
<Stack gap={"xs"}>
<HeaderSearch
title='Detail Data Pengangguran'
placeholder='pencarian'
placeholder='Cari usia...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGrafikBerdasarkanUsiaKerjaYangMenganggur search={search} />
</Stack>
<ListGrafikBerdasarkanUsiaKerjaYangMenganggur search={search} />
</Box>
);
}
function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({search}: {search: string}) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur)
function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const handleDelete = async () => {
if (selectedId) {
await stategrafik.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stategrafik.findMany.load()
await stategrafik.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
stategrafik.findMany.load();
}
}
};
const {
data,
page,
totalPages,
loading,
load,
} = stategrafik.findMany;
useShallowEffect(() => {
setMounted(true);
stategrafik.findMany.load()
}, []);
load(page, 10, search)
}, [page, search]);
useEffect(() => {
if (stategrafik.findMany.data) {
@@ -69,139 +71,149 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({search}: {search: string}
{ name: 'usia46_keatas', value: totalUsia46_keatas, color: '#1094A8FF', key: 'usia46_keatas' },
]);
}
}, [stategrafik.findMany.data])
}, [stategrafik.findMany.data]);
const filteredData = (stategrafik.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
const filteredData = data || []
if (loading || !data) {
return (
item.usia18_25.toString().toLowerCase().includes(keyword) ||
item.usia26_35.toString().toLowerCase().includes(keyword) ||
item.usia36_45.toString().toLowerCase().includes(keyword) ||
item.usia46_keatas.toString().toLowerCase().includes(keyword)
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
});
if (!stategrafik.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}>
<JudulList
title='List Pengangguran Berdasarkan Usia Kerja'
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Usia 18-25</TableTh>
<TableTh>Usia 26-35</TableTh>
<TableTh>Usia 36-45</TableTh>
<TableTh>Usia 46 +</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack>
{/* Header */}
<Flex justify="space-between" align="center" mb="md">
<Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
<Tooltip label="Tambah Data" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Flex>
{/* Table */}
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTd colSpan={6}>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text>
</TableTd>
<TableTh>Usia 18-25</TableTh>
<TableTh>Usia 26-35</TableTh>
<TableTh>Usia 36-45</TableTh>
<TableTh>Usia 46 +</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.usia18_25}</TableTd>
<TableTd>{item.usia26_35}</TableTd>
<TableTd>{item.usia36_45}</TableTd>
<TableTd>{item.usia46_keatas}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map(item => (
<TableTr key={item.id}>
<TableTd>{item.usia18_25}</TableTd>
<TableTd>{item.usia26_35}</TableTd>
<TableTd>{item.usia36_45}</TableTd>
<TableTd>{item.usia46_keatas}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" disabled={stategrafik.delete.loading} onClick={() => { setSelectedId(item.id); setModalHapus(true); }}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={6}>
<Center py={20}>
<Text color="dimmed">Belum ada data grafik responden</Text>
</Center>
</TableTd>
</TableTr>
))
)}
</TableTbody>
)}
</TableTbody>
</Table>
</Box>
</Table>
</Paper>
</Stack>
</Paper>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Pengangguran Berdasarkan Usia Kerja</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}
data={donutData}
>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
cx={500}
cy={150}
innerRadius={60}
outerRadius={115}
label={true}
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<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>
{/* Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack>
<Title order={3} pb={10}>Grafik Pengangguran Berdasarkan Usia Kerja</Title>
{mounted && donutData.length > 0 ? (
<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart width={800} height={300} data={donutData}>
<Pie dataKey="value" nameKey="name" data={donutData} cx={400} cy={150} innerRadius={60} outerRadius={115} label>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Stack mt="sm" gap="xs">
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>Usia 18-25 : {donutData.find((entry) => entry.name === 'usia18_25')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} />
<Text>Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value}</Text>
<Text>Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value}
</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#C07B13FF'} w={20} h={20} />
<Text>Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value}</Text>
<Text>Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value}
</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#1094A8FF'} w={20} h={20} />
<Text>Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value}</Text>
<Text>Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value}
</Text>
</Flex>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
</Box>
</Stack>
</Stack>
</Box>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan usia kerja ini?'
text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan usia kerja ini?"
/>
</Box>
);

View File

@@ -1,8 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
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, Select, NumberInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -10,75 +10,80 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditDetailDataPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const router = useRouter();
const params = useParams()
const params = useParams();
const [formData, setFormData] = useState({
month: stateDetail.update.form.month,
year: stateDetail.update.form.year,
totalUnemployment: stateDetail.update.form.totalUnemployment,
educatedUnemployment: stateDetail.update.form.educatedUnemployment,
uneducatedUnemployment: stateDetail.update.form.uneducatedUnemployment,
percentageChange: stateDetail.update.form.percentageChange || 0, // Ensure it's always a number
})
month: '',
year: new Date().getFullYear(),
educatedUnemployment: 0,
uneducatedUnemployment: 0,
totalUnemployment: 0,
percentageChange: 0,
});
// Hitung total & perubahan otomatis
const calculateTotalAndChange = async () => {
const total = formData.educatedUnemployment + formData.uneducatedUnemployment;
// Ambil data bulan sebelumnya
const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const currentIndex = monthOrder.findIndex(
(m) => m.toLowerCase() === formData.month.toLowerCase()
);
let percentageChange = 0;
if (currentIndex > 0) {
const prevMonth = monthOrder[currentIndex - 1];
const prev = await stateDetail.findByMonthYear.load({
month: prevMonth,
year: formData.year,
});
const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const currentMonthIndex = monthOrder.indexOf(formData.month);
if (prev?.totalUnemployment) {
percentageChange = Number(
(((total - prev.totalUnemployment) / prev.totalUnemployment) * 100).toFixed(1)
);
if (currentMonthIndex !== -1) {
let prevMonthIndex = currentMonthIndex - 1;
let prevYear = formData.year;
if (prevMonthIndex < 0) {
prevMonthIndex = 11;
prevYear--;
}
const prevMonth = monthOrder[prevMonthIndex];
const prevData = await stateDetail.findByMonthYear.load({ month: prevMonth, year: prevYear });
if (prevData && prevData.totalUnemployment > 0) {
const change = ((total - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
percentageChange = parseFloat(change.toFixed(1));
}
}
setFormData({
...formData,
totalUnemployment: total,
percentageChange,
});
return { total, percentageChange };
};
const updateFormData = async (updates: Partial<typeof formData>) => {
const newData = { ...formData, ...updates };
const { total, percentageChange } = await calculateTotalAndChange();
setFormData({ ...newData, totalUnemployment: total, percentageChange });
};
useEffect(() => {
const loadDetail = async () => {
const id = params?.id as string;
if (!id) return;
try {
await stateDetail.findUnique.load(id); // ambil by ID
const data = stateDetail.findUnique.data;
if (data) {
// Convert year from Date to number
const yearValue = data.year instanceof Date ? data.year.getFullYear() : data.year;
// Convert year from Date to number if needed
const yearValue = data.year && typeof data.year === 'object' && 'getFullYear' in data.year
? (data.year as Date).getFullYear()
: Number(data.year);
// Set the ID for update
stateDetail.update.id = id;
// Update Valtio state with converted year
stateDetail.update.form = {
stateDetail.update.form = {
...data,
year: yearValue,
percentageChange: data.percentageChange || 0 // Ensure it's always a number
};
// Update local formData with converted year
setFormData({
month: data.month,
@@ -94,90 +99,76 @@ function EditDetailDataPengangguran() {
toast.error("Gagal memuat data detail");
}
};
loadDetail();
}, [params?.id]);
const handleSubmit = async () => {
const { total, percentageChange } = await calculateTotalAndChange();
try {
stateDetail.update.form = {
...formData,
totalUnemployment: total,
percentageChange,
};
stateDetail.update.form = { ...formData, totalUnemployment: total, percentageChange };
const success = await stateDetail.update.submit();
if (success) {
toast.success("Detail data pengangguran berhasil diperbarui!");
router.push("/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran");
toast.success('Detail data pengangguran berhasil diperbarui!');
router.push('/admin/ekonomi/jumlah-pengangguran');
}
} catch (error) {
console.error("Error updating:", error);
toast.error("Terjadi kesalahan saat memperbarui data");
console.error('Error updating:', error);
toast.error('Terjadi kesalahan saat memperbarui data');
}
}
};
return (
<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">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Box>
<Title order={4} ml="sm">
Edit Detail Data Pengangguran
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Edit Detail Data Pengangguran</Title>
<Stack gap="xs">
<TextInput
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p="lg" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Stack gap="md">
<Select
label="Bulan"
data={['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']}
value={formData.month}
placeholder="Contoh: Jan, Feb, Mar"
onChange={(val) => (setFormData({
...formData,
month: val.currentTarget.value
}))}
onChange={(val) => updateFormData({ month: val || '' })}
/>
<TextInput
<NumberInput
label="Tahun"
type="number"
value={formData.year}
onChange={(val) => (setFormData({
...formData,
year: Number(val.currentTarget.value)
}))}
onChange={(val) => updateFormData({ year: Number(val) })}
/>
<TextInput
label="Pengangguran Terdidik"
type="number"
value={formData.educatedUnemployment}
onChange={(val) => (setFormData({
...formData,
educatedUnemployment: Number(val.currentTarget.value)
}))}
onChange={(val) => updateFormData({ educatedUnemployment: Number(val.currentTarget.value) || 0 })}
/>
<TextInput
label="Pengangguran Tidak Terdidik"
type="number"
value={formData.uneducatedUnemployment}
onChange={(val) => (setFormData({
...formData,
uneducatedUnemployment: Number(val.currentTarget.value)
}))}
onChange={(val) => updateFormData({ uneducatedUnemployment: Number(val.currentTarget.value) || 0 })}
/>
<Text fz="sm" fw={500}>
Total Otomatis: {formData.totalUnemployment}
</Text>
<Text fz="sm" fw={500}>
Perubahan Otomatis:{" "}
{formData.percentageChange !== null
? `${formData.percentageChange}%`
: '-'}
</Text>
<Group>
<Button bg={colors['blue-button']} mt={10} onClick={handleSubmit}>
Submit
<Text fz="sm" fw={500}>Total Otomatis: {formData.totalUnemployment}</Text>
<Text fz="sm" fw={500}>Perubahan Otomatis: {formData.percentageChange !== null ? `${formData.percentageChange}%` : '-'}</Text>
<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>
@@ -187,3 +178,4 @@ function EditDetailDataPengangguran() {
}
export default EditDetailDataPengangguran;

View File

@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Flex, 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';
@@ -11,86 +11,125 @@ import { useProxy } from 'valtio/utils';
function DetailJumlahPengangguran() {
const router = useRouter();
const params = useParams()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const params = useParams();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
useShallowEffect(() => {
stateDetail.findUnique.load(params?.id as string)
}, [params?.id])
stateDetail.findUnique.load(params?.id as string);
}, [params?.id]);
const handleHapus = () => {
if (selectedId) {
stateDetail.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/jumlah-pengangguran")
stateDetail.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/ekonomi/jumlah-pengangguran");
}
}
};
if (!stateDetail.findUnique.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = stateDetail.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 Data Pengangguran</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Paper Detail */}
<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 Data Pengangguran
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fw={"bold"}>Pengangguran Terdidik</Text>
<Text>{stateDetail.findUnique.data?.educatedUnemployment}</Text>
<Text fw="bold">Pengangguran Terdidik</Text>
<Text c="dimmed">{data.educatedUnemployment || '-'}</Text>
</Box>
<Box>
<Text fw={"bold"}>Pengangguran Tidak Terdidik</Text>
<Text>{stateDetail.findUnique.data?.uneducatedUnemployment}</Text>
<Text fw="bold">Pengangguran Tidak Terdidik</Text>
<Text c="dimmed">{data.uneducatedUnemployment || '-'}</Text>
</Box>
<Box>
<Text fw={"bold"}>Perubahan</Text>
<Text>{stateDetail.findUnique.data?.percentageChange}</Text>
<Text fw="bold">Perubahan</Text>
<Text c="dimmed">
{data.percentageChange !== null && data.percentageChange !== undefined
? `${data.percentageChange}%`
: 'Tidak ada data perubahan'}
</Text>
</Box>
<Box>
<Text fw={"bold"}>Tahun</Text>
<Text>{stateDetail.findUnique.data?.year ? new Date(stateDetail.findUnique.data.year).getFullYear() : ''}</Text>
<Text fw="bold">Tahun</Text>
<Text c="dimmed">{data.year || '-'}</Text>
</Box>
<Box>
<Text fw={"bold"}>Bulan</Text>
<Text>{stateDetail.findUnique.data?.month}</Text>
<Text fw="bold">Bulan</Text>
<Text c="dimmed">{data.month || '-'}</Text>
</Box>
<Box>
<Text fw={"bold"}>Total Pengangguran</Text>
<Text>{stateDetail.findUnique.data?.totalUnemployment}</Text>
<Text fw="bold">Total Pengangguran</Text>
<Text c="dimmed">{data.totalUnemployment || '-'}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
{/* Tombol Edit & Hapus */}
<Flex gap="sm">
<Tooltip label="Hapus Data Pengangguran" withArrow position="top">
<Button
onClick={() => {
if (stateDetail.findUnique.data) {
setSelectedId(stateDetail.findUnique.data.id);
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}
}}
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
color={"red"}>
}}
color="red"
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green">
</Tooltip>
<Tooltip label="Edit Data Pengangguran" withArrow position="top">
<Button
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${data.id}/edit`)}
color="green"
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Flex>
</Stack>
</Paper>
</Stack>
@@ -101,7 +140,7 @@ function DetailJumlahPengangguran() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus data ini?"
text="Apakah Anda yakin ingin menghapus data ini?"
/>
</Box>
);

View File

@@ -3,27 +3,43 @@
'use client'
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
NumberInput,
Title,
Select,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function CreateJumlahPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter();
const monthOptions = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
];
const resetForm = () => {
stateDetail.create.form = {
month: "",
year: 0,
month: monthOptions[new Date().getMonth()], // default bulan sekarang
year: new Date().getFullYear(), // default tahun sekarang
totalUnemployment: 0,
educatedUnemployment: 0,
uneducatedUnemployment: 0,
percentageChange: 0,
}
}
};
};
const calculateTotalAndChange = async () => {
const total =
@@ -32,11 +48,8 @@ function CreateJumlahPengangguran() {
stateDetail.create.form.totalUnemployment = total;
// Ambil data bulan sebelumnya
const monthOrder = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
];
// hitung perubahan dibanding bulan sebelumnya
const monthOrder = monthOptions;
const currentIndex = monthOrder.findIndex(
(m) => m.toLowerCase() === stateDetail.create.form.month.toLowerCase()
);
@@ -68,66 +81,117 @@ function CreateJumlahPengangguran() {
setChartData([stateDetail.findUnique.data]);
}
resetForm();
router.push('/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran');
router.push('/admin/ekonomi/jumlah-pengangguran');
}
};
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'}>
<Title order={4}>Tambah Detail Data Pengangguran</Title>
<Stack gap="xs">
<TextInput
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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 Data Pengangguran
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Select
label="Bulan"
placeholder="Pilih bulan"
data={monthOptions}
value={stateDetail.create.form.month}
placeholder="Contoh: Jan, Feb, Mar"
onChange={(e) => (stateDetail.create.form.month = e.currentTarget.value)}
onChange={(value) => {
stateDetail.create.form.month = value || '';
calculateTotalAndChange();
}}
required
/>
<TextInput
<NumberInput
label="Tahun"
type="date"
value={stateDetail.create.form.year}
onChange={(e) =>
(stateDetail.create.form.year = Number(e.currentTarget.value))
}
onChange={(value) => {
stateDetail.create.form.year = Number(value) || new Date().getFullYear();
calculateTotalAndChange();
}}
min={2000}
max={2100}
required
/>
<TextInput
<NumberInput
label="Pengangguran Terdidik"
type="number"
value={stateDetail.create.form.educatedUnemployment}
onChange={(e) => {
stateDetail.create.form.educatedUnemployment = Number(
e.currentTarget.value,
);
onChange={(value) => {
stateDetail.create.form.educatedUnemployment = Number(value) || 0;
calculateTotalAndChange();
}}
min={0}
required
/>
<TextInput
<NumberInput
label="Pengangguran Tidak Terdidik"
type="number"
value={stateDetail.create.form.uneducatedUnemployment}
onChange={(e) => {
stateDetail.create.form.uneducatedUnemployment = Number(
e.currentTarget.value,
);
onChange={(value) => {
stateDetail.create.form.uneducatedUnemployment = Number(value) || 0;
calculateTotalAndChange();
}}
min={0}
required
/>
<Text fz="sm" fw={500}>
Total Otomatis: {stateDetail.create.form.totalUnemployment}
</Text>
<Text fz="sm" fw={500}>
Perubahan Otomatis:{" "}
{stateDetail.create.form.percentageChange !== null
? `${stateDetail.create.form.percentageChange}%`
: '-'}
</Text>
<Group>
<Button bg={colors['blue-button']} mt={10} onClick={handleSubmit}>
Submit
<Box>
<Text fz="sm" fw={500} mb={4}>
Total Otomatis:
</Text>
<Text fz="sm" c="dimmed">
{stateDetail.create.form.totalUnemployment.toLocaleString()}
</Text>
</Box>
<Box>
<Text fz="sm" fw={500} mb={4}>
Perubahan Otomatis:
</Text>
<Text fz="sm" c="dimmed">
{stateDetail.create.form.percentageChange.toFixed(1)}%
</Text>
</Box>
{/* Action 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={!stateDetail.create.form.month || !stateDetail.create.form.year}
>
Simpan
</Button>
</Group>
</Stack>

View File

@@ -1,150 +1,181 @@
/* 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, Text, Title } 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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { BarChart } from '@mantine/charts';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran';
function DetailDataPengangguran() {
const [search, setSearch] = useState("")
const [search, setSearch] = useState("");
return (
<Box>
<Stack gap={"xs"}>
<HeaderSearch
title='Detail Data Pengangguran'
placeholder='pencarian'
placeholder='Cari bulan atau tahun...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListDetailDataPengangguran search={search} />
</Stack>
<ListDetailDataPengangguran search={search} />
</Box>
);
}
function ListDetailDataPengangguran({search}: {search: string}) {
type DetailDataPengangguran = {
id: string;
month: string;
year: number;
educatedUnemployment: number;
uneducatedUnemployment: number;
percentageChange: number;
totalUnemployment: number;
}
const [chartData, setChartData] = useState<DetailDataPengangguran[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
function ListDetailDataPengangguran({ search }: { search: string }) {
const [chartData, setChartData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = stateDetail.findMany;
useShallowEffect(() => {
setMounted(true)
stateDetail.findMany.load()
}, [])
setMounted(true);
load(page, 10, search);
}, [page, search]);
useEffect(() => {
setMounted(true);
if (stateDetail.findMany.data) {
setChartData(stateDetail.findMany.data.map((item) => ({
id: item.id,
month: item.month,
year: item.year instanceof Date ? item.year.getFullYear() : Number(item.year),
educatedUnemployment: Number(item.educatedUnemployment),
uneducatedUnemployment: Number(item.uneducatedUnemployment),
percentageChange: Number(item.percentageChange),
totalUnemployment: Number(item.totalUnemployment),
})));
if (data) {
setChartData(
data.map((item) => ({
id: item.id,
month: item.month,
year: Number(item.year),
educatedUnemployment: Number(item.educatedUnemployment),
uneducatedUnemployment: Number(item.uneducatedUnemployment),
percentageChange: Number(item.percentageChange),
totalUnemployment: Number(item.totalUnemployment),
}))
);
}
}, [stateDetail.findMany.data]);
}, [data]);
const filteredData = (stateDetail.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
const filteredData = data || []
// Loading state
if (loading || !data) {
return (
item.month.toLowerCase().includes(keyword) ||
item.year.toString().toLowerCase().includes(keyword)
<Stack py="md">
<Skeleton h={500} radius="md" />
</Stack>
);
});
if (!stateDetail.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Stack gap={"md"}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Detail Data Pengangguran'
href='/admin/ekonomi/jumlah-pengangguran/create'
/>
<Table striped withTableBorder withRowBorders>
<Stack py="md" gap="lg">
{/* Table Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Detail Data Pengangguran</Title>
<Tooltip label="Tambah Data Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-pengangguran/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Bulan</TableTh>
<TableTh>Terdidik</TableTh>
<TableTh>Tidak Terdidik</TableTh>
<TableTh>Detail</TableTh>
<TableTh style={{ width: '25%' }}>Bulan</TableTh>
<TableTh style={{ width: '20%' }}>Terdidik</TableTh>
<TableTh style={{ width: '20%' }}>Tidak Terdidik</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.month}</TableTd>
<TableTd>{item.educatedUnemployment}</TableTd>
<TableTd>{item.uneducatedUnemployment}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.month} {item.year}</TableTd>
<TableTd>{item.educatedUnemployment}</TableTd>
<TableTd>{item.uneducatedUnemployment}</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data yang cocok</Text>
</Center>
</TableTd>
</TableTr>
))}
)}
</TableTbody>
</Table>
</Paper>
</Box>
</Paper>
<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>
{/* 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}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
{/* Chart Section */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Title order={4} mb="md">
Data Pengangguran Terdidik & Tidak Terdidik
</Title>
{mounted && chartData.length > 0 ? (
<Box w={{ base: '100%', md: '70%' }}>
<BarChart
h={450}
data={chartData}
dataKey="month"
series={[
{ name: 'educatedUnemployment', color: 'red.6', label: 'Terdidik' },
{ name: 'uneducatedUnemployment', color: 'orange.6', label: 'Tidak Terdidik' },
]}
/>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
{mounted && chartData.length > 0 && (
<Box w={{ base: '100%', md: '70%' }}>
<BarChart
h={450}
data={chartData}
dataKey="month"
series={[
{ name: 'educatedUnemployment', color: 'red.6', label: 'Terdidik' },
{ name: 'uneducatedUnemployment', color: 'orange.6', label: 'Tidak Terdidik' },
]}
/>
</Box>
)}
</Paper>
</Box>
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Box>
</Paper>
</Stack>
);
}

View File

@@ -3,16 +3,25 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
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 { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState)
const lowonganState = useProxy(lowonganKerjaState);
const router = useRouter();
const params = useParams();
@@ -24,7 +33,7 @@ function EditLowonganKerja() {
gaji: lowonganKerjaState.update.form.gaji,
deskripsi: lowonganKerjaState.update.form.deskripsi,
kualifikasi: lowonganKerjaState.update.form.kualifikasi,
})
});
useEffect(() => {
const loadLowongan = async () => {
@@ -32,7 +41,7 @@ function EditLowonganKerja() {
if (!id) return;
try {
const data = await lowonganState.update.load(id); // akses langsung, bukan dari proxy
const data = await lowonganState.update.load(id);
if (data) {
setFormData({
posisi: data.posisi || '',
@@ -51,20 +60,17 @@ function EditLowonganKerja() {
};
loadLowongan();
}, [params?.id])
}, [params?.id]);
const handleSubmit = async () => {
try {
// Set the ID for the update
lowonganState.update.id = params?.id as string;
// Update the form state
lowonganState.update.form = {
...lowonganState.update.form,
...formData
...formData,
};
// Call the update function
await lowonganState.update.update();
toast.success("Lowongan kerja berhasil diperbarui!");
router.push("/admin/ekonomi/lowongan-kerja-lokal");
@@ -72,79 +78,105 @@ function EditLowonganKerja() {
console.error("Error updating lowongan kerja:", error);
toast.error("Terjadi kesalahan saat memperbarui lowongan kerja");
}
}
};
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">
{/* Header dengan tombol back */}
<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 Lowongan Kerja Lokal
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Lowongan Kerja Lokal</Title>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Posisi"
placeholder="Masukkan posisi"
value={formData.posisi}
onChange={(val) => {
setFormData(prev => ({ ...prev, posisi: val.target.value }));
}}
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>}
placeholder='Masukkan posisi'
onChange={(e) => setFormData({ ...formData, posisi: e.target.value })}
required
/>
<TextInput
label="Nama Perusahaan"
placeholder="Masukkan nama perusahaan"
value={formData.namaPerusahaan}
onChange={(val) => {
setFormData(prev => ({ ...prev, namaPerusahaan: val.target.value }));
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>}
placeholder='Masukkan nama perusahaan'
onChange={(e) => setFormData({ ...formData, namaPerusahaan: e.target.value })}
required
/>
<TextInput
label="Lokasi"
placeholder="Masukkan lokasi"
value={formData.lokasi}
onChange={(val) => {
setFormData(prev => ({ ...prev, lokasi: val.target.value }));
}}
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
placeholder='Masukkan lokasi'
onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
required
/>
<TextInput
label="Tipe Pekerjaan"
placeholder="Masukkan tipe pekerjaan"
value={formData.tipePekerjaan}
onChange={(val) => {
setFormData(prev => ({ ...prev, tipePekerjaan: val.target.value }));
}}
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>}
placeholder='Masukkan tipe pekerjaan'
onChange={(e) => setFormData({ ...formData, tipePekerjaan: e.target.value })}
required
/>
<TextInput
label="Gaji (per bulan)"
placeholder="Masukkan gaji"
value={formData.gaji}
onChange={(val) => {
setFormData(prev => ({ ...prev, gaji: val.target.value }));
}}
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
placeholder='Masukkan gaji'
onChange={(e) => setFormData({ ...formData, gaji: e.target.value })}
required
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Lowongan Kerja
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData(prev => ({ ...prev, deskripsi: val }));
}}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Kualifikasi Lowongan Kerja</Text>
<Text fw="bold" fz="sm" mb={6}>
Kualifikasi Lowongan Kerja
</Text>
<EditEditor
value={formData.kualifikasi}
onChange={(val) => {
setFormData(prev => ({ ...prev, kualifikasi: val }));
}}
onChange={(val) => setFormData({ ...formData, kualifikasi: val })}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>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>

View File

@@ -1,128 +1,148 @@
'use client'
import { useProxy } from 'valtio/utils';
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';
import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
function DetailLowonganKerjaLokal() {
const lowonganState = useProxy(lowonganKerjaState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
const lowonganState = useProxy(lowonganKerjaState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
lowonganState.findUnique.load(params?.id as string)
}, [])
lowonganState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
lowonganState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/lowongan-kerja-lokal")
lowonganState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/ekonomi/lowongan-kerja-lokal");
}
}
};
if (!lowonganState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = lowonganState.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 Lowongan Kerja Lokal</Text>
{lowonganState.findUnique.data ? (
<Paper key={lowonganState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Bekerja Sebagai</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.posisi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Usaha</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.namaPerusahaan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Lokasi</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.lokasi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tipe Pekerjaan</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.tipePekerjaan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gaji</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.gaji}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: lowonganState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kualifikasi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: lowonganState.findUnique.data?.kualifikasi }} />
</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: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Lowongan Kerja Lokal
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Bekerja Sebagai</Text>
<Text fz="md" c="dimmed">{data.posisi || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Nama Usaha</Text>
<Text fz="md" c="dimmed">{data.namaPerusahaan || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Lokasi</Text>
<Text fz="md" c="dimmed">{data.lokasi || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tipe Pekerjaan</Text>
<Text fz="md" c="dimmed">{data.tipePekerjaan || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Gaji</Text>
<Text fz="md" c="dimmed">{data.gaji || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Kualifikasi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }} />
</Box>
<Group gap="sm" mt="sm">
<Tooltip label="Hapus Lowongan" withArrow position="top">
<Button
color="red"
onClick={() => {
if (lowonganState.findUnique.data) {
setSelectedId(lowonganState.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={lowonganState.delete.loading || !lowonganState.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Lowongan" withArrow position="top">
<Button
onClick={() => {
if (lowonganState.findUnique.data) {
router.push(`/admin/ekonomi/lowongan-kerja-lokal/${lowonganState.findUnique.data.id}/edit`);
}
}}
disabled={!lowonganState.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${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 lowongan kerja ini?'
text="Apakah Anda yakin ingin menghapus lowongan kerja ini?"
/>
</Box>
);
}
export default DetailLowonganKerjaLokal;
export default DetailLowonganKerjaLokal;

View File

@@ -1,88 +1,123 @@
'use client'
'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,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
function CreateLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState)
const lowonganState = useProxy(lowonganKerjaState);
const router = useRouter();
const resetForm = () => {
lowonganState.create.form = {
posisi: "",
namaPerusahaan: "",
lokasi: "",
tipePekerjaan: "",
gaji: "",
deskripsi: "",
kualifikasi: "",
}
}
posisi: '',
namaPerusahaan: '',
lokasi: '',
tipePekerjaan: '',
gaji: '',
deskripsi: '',
kualifikasi: '',
};
};
const handleSubmit = async () => {
await lowonganState.create.create()
resetForm()
router.push("/admin/ekonomi/lowongan-kerja-lokal")
}
await lowonganState.create.create();
resetForm();
router.push('/admin/ekonomi/lowongan-kerja-lokal');
};
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">
{/* Header dengan tombol kembali */}
<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 Lowongan Kerja Lokal
</Title>
</Group>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Lowongan Kerja Lokal</Title>
{/* Card Form */}
<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
value={lowonganState.create.form.posisi}
onChange={(val) => {
lowonganState.create.form.posisi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>}
placeholder='Masukkan posisi'
value={lowonganState.create.form.posisi}
onChange={(val) =>
(lowonganState.create.form.posisi = val.target.value)
}
label="Posisi"
placeholder="Masukkan posisi"
required
/>
<TextInput
value={lowonganState.create.form.namaPerusahaan}
onChange={(val) => {
lowonganState.create.form.namaPerusahaan = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>}
placeholder='Masukkan nama perusahaan'
value={lowonganState.create.form.namaPerusahaan}
onChange={(val) =>
(lowonganState.create.form.namaPerusahaan = val.target.value)
}
label="Nama Perusahaan"
placeholder="Masukkan nama perusahaan"
required
/>
<TextInput
value={lowonganState.create.form.lokasi}
onChange={(val) => {
lowonganState.create.form.lokasi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
placeholder='Masukkan lokasi'
value={lowonganState.create.form.lokasi}
onChange={(val) =>
(lowonganState.create.form.lokasi = val.target.value)
}
label="Lokasi"
placeholder="Masukkan lokasi"
required
/>
<TextInput
value={lowonganState.create.form.tipePekerjaan}
onChange={(val) => {
lowonganState.create.form.tipePekerjaan = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>}
placeholder='Masukkan tipe pekerjaan'
value={lowonganState.create.form.tipePekerjaan}
onChange={(val) =>
(lowonganState.create.form.tipePekerjaan = val.target.value)
}
label="Tipe Pekerjaan"
placeholder="Masukkan tipe pekerjaan"
required
/>
<TextInput
value={lowonganState.create.form.gaji}
onChange={(val) => {
lowonganState.create.form.gaji = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
placeholder='Masukkan gaji'
value={lowonganState.create.form.gaji}
onChange={(val) =>
(lowonganState.create.form.gaji = val.target.value)
}
label="Gaji (per bulan)"
placeholder="Masukkan gaji"
required
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Lowongan Kerja
</Text>
<CreateEditor
value={lowonganState.create.form.deskripsi}
onChange={(val) => {
@@ -90,8 +125,11 @@ function CreateLowonganKerja() {
}}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Kualifikasi Lowongan Kerja</Text>
<Text fw="bold" fz="sm" mb={6}>
Kualifikasi Lowongan Kerja
</Text>
<CreateEditor
value={lowonganState.create.form.kualifikasi}
onChange={(val) => {
@@ -99,12 +137,25 @@ function CreateLowonganKerja() {
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
{/* Tombol Simpan */}
<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>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

@@ -1,22 +1,40 @@
'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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import lowonganKerjaState from '../../_state/ekonomi/lowongan-kerja';
import { useProxy } from 'valtio/utils';
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 { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import lowonganKerjaState from '../../_state/ekonomi/lowongan-kerja';
function LowonganKerjaLokal() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Lowongan Kerja Lokal'
placeholder='pencarian'
title="Lowongan Kerja Lokal"
placeholder="Cari pekerjaan atau perusahaan..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,74 +45,117 @@ function LowonganKerjaLokal() {
}
function ListLowonganKerjaLokal({ search }: { search: string }) {
const lowonganState = useProxy(lowonganKerjaState)
const stateLowongan = useProxy(lowonganKerjaState);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = lowonganState.findMany
const { data, page, totalPages, loading, load } = stateLowongan.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = 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 Lowongan Kerja Lokal'
href='/admin/ekonomi/lowongan-kerja-lokal/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Pekerjaan</TableTh>
<TableTh>Nama Perusahaan</TableTh>
<TableTh>Lokasi</TableTh>
<TableTh>Detail</TableTh>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Lowongan Kerja Lokal</Title>
<Tooltip label="Tambah Lowongan Kerja" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/ekonomi/lowongan-kerja-lokal/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Pekerjaan</TableTh>
<TableTh style={{ width: '25%' }}>Nama Perusahaan</TableTh>
<TableTh style={{ width: '20%' }}>Lokasi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fz={"md"}>{item.posisi}</Text>
</TableTd>
<TableTd>
<Text fz={"md"}>{item.namaPerusahaan}</Text>
</TableTd>
<TableTd>
<Text fz={"md"}>{item.lokasi}</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.posisi}
</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Text truncate fz="sm" c="dimmed">
{item.namaPerusahaan}
</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text truncate fz="sm" c="dimmed">
{item.lokasi}
</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/ekonomi/lowongan-kerja-lokal/${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 lowongan kerja yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
my="md"
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -1,62 +1,131 @@
/* 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,
ScrollArea,
} from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconShoppingBag, IconCategory } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Produk Pasar Desa",
value: "produkpasardesa",
href: "/admin/ekonomi/pasar-desa/produk-pasar-desa"
},
{
label: "Kategori Produk",
value: "kategoriproduk",
href: "/admin/ekonomi/pasar-desa/kategori-produk"
},
];
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: "Produk Pasar Desa",
value: "produkpasardesa",
href: "/admin/ekonomi/pasar-desa/produk-pasar-desa",
icon: <IconShoppingBag size={18} stroke={1.8} />,
tooltip: "Kelola data produk yang ada di pasar desa",
},
{
label: "Kategori Produk",
value: "kategoriproduk",
href: "/admin/ekonomi/pasar-desa/kategori-produk",
icon: <IconCategory size={18} stroke={1.8} />,
tooltip: "Atur kategori produk pasar 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}>Pasar 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" }}>
Pasar Desa
</Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{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",
flexShrink: 0,
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{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;
export default LayoutTabs;

View File

@@ -5,7 +5,17 @@ import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Title,
TextInput,
Tooltip,
Text,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { toast } from 'react-toastify';
@@ -16,7 +26,7 @@ function EditKategoriProduk() {
const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [formData, setFormData] = useState({
nama: "",
nama: '',
});
useEffect(() => {
@@ -34,8 +44,8 @@ function EditKategoriProduk() {
});
}
} catch (error) {
console.error("Error loading kategori produk:", error);
toast.error("Gagal memuat data kategori produk");
console.error('Error loading kategori produk:', error);
toast.error('Gagal memuat data kategori produk');
}
};
@@ -53,7 +63,6 @@ function EditKategoriProduk() {
nama: formData.nama.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!statePasar.edit.id) {
statePasar.edit.id = id; // fallback
}
@@ -61,33 +70,60 @@ function EditKategoriProduk() {
const success = await statePasar.edit.update();
if (success) {
router.push("/admin/ekonomi/pasar-desa/kategori-produk");
toast.success('Kategori produk berhasil diperbarui!');
router.push('/admin/ekonomi/pasar-desa/kategori-produk');
}
} catch (error) {
console.error("Error updating kategori produk:", error);
// toast akan ditampilkan dari fungsi update
console.error('Error updating kategori produk:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori produk');
}
};
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">
{/* Header dengan tombol back */}
<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 Produk
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Kategori Produk</Title>
{/* Card form */}
<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 fw="bold" fz="sm">Nama Kategori Produk</Text>}
placeholder="Masukkan nama kategori produk"
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Produk</Text>}
placeholder='Masukkan nama kategori produk'
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>

View File

@@ -1,16 +1,26 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React, { useEffect } from 'react';
'use client';
import colors from '@/con/colors';
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 { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function CreateKategoriProduk() {
const router = useRouter();
const statePasar = useProxy(pasarDesaState.kategoriProduk)
const statePasar = useProxy(pasarDesaState.kategoriProduk);
useEffect(() => {
statePasar.findMany.load();
@@ -18,42 +28,73 @@ function CreateKategoriProduk() {
const resetForm = () => {
statePasar.create.form = {
nama: "",
nama: '',
};
}
};
const handleSubmit = async () => {
if (!statePasar.create.form.nama) {
return toast.warn('Nama kategori produk wajib diisi');
}
await statePasar.create.create();
resetForm();
router.push("/admin/ekonomi/pasar-desa/kategori-produk")
}
router.push('/admin/ekonomi/pasar-desa/kategori-produk');
};
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">
{/* Header dengan tombol kembali */}
<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 Produk
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kategori Produk</Title>
<TextInput
value={statePasar.create.form.nama}
onChange={(val) => {
statePasar.create.form.nama = val.target.value;
{/* Card form */}
<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 Produk"
placeholder="Masukkan nama kategori produk"
value={statePasar.create.form.nama || ''}
onChange={(e) => (statePasar.create.form.nama = e.target.value)}
required
/>
<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)',
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Produk</Text>}
placeholder='Masukkan nama kategori produk'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,38 +1,36 @@
'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, 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 pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
function PasarDesa() {
function KategoriProduk() {
const [search2, setSearch2] = useState("")
return (
<Box>
<HeaderSearch
title='Kategori Produk'
placeholder='pencarian'
placeholder='Cari nama kategori produk...'
searchIcon={<IconSearch size={20} />}
value={search2}
onChange={(e) => setSearch2(e.currentTarget.value)}
/>
<ListPasarDesa search2={search2} />
<ListKategoriProduk search2={search2} />
</Box>
);
}
function ListPasarDesa({ search2 }: { search2: string }) {
function ListKategoriProduk({ search2 }: { search2: string }) {
const statePasar = useProxy(pasarDesaState.kategoriProduk)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
// const params = useParams()
const router = useRouter()
const {
@@ -60,56 +58,102 @@ function ListPasarDesa({ search2 }: { search2: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Produk Pasar Desa'
href='/admin/ekonomi/pasar-desa/kategori-produk/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</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/ekonomi/pasar-desa/kategori-produk/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Kategori Produk</Title>
<Tooltip label="Tambah Kategori Produk" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/pasar-desa/kategori-produk/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '60%' }}>Nama Kategori</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Delete</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</TableTd>
<TableTd>
<Tooltip label="Edit Kategori Produk" withArrow>
<Button
color="green"
variant="light"
onClick={() => router.push(`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus Kategori Produk" withArrow>
<Button
color="red"
variant="light"
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}
>
<IconX size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data kategori produk yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
onChange={(newPage) => {
load(newPage, 10)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
total={totalPages}
my={"md"}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
@@ -118,7 +162,7 @@ function ListPasarDesa({ search2 }: { search2: string }) {
text='Apakah anda yakin ingin menghapus kategori produk ini?'
/>
</Box>
);
)
}
export default PasarDesa;
export default KategoriProduk;

View File

@@ -1,10 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
MultiSelect,
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';
@@ -13,20 +25,20 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPasarDesa() {
const pasarState = useProxy(pasarDesaState)
const pasarState = useProxy(pasarDesaState);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
nama: pasarState.pasarDesa.edit.form.nama || "",
nama: pasarState.pasarDesa.edit.form.nama || '',
harga: pasarState.pasarDesa.edit.form.harga || 0,
alamatUsaha: pasarState.pasarDesa.edit.form.alamatUsaha || "",
imageId: pasarState.pasarDesa.edit.form.imageId || "",
alamatUsaha: pasarState.pasarDesa.edit.form.alamatUsaha || '',
imageId: pasarState.pasarDesa.edit.form.imageId || '',
rating: pasarState.pasarDesa.edit.form.rating || 0,
kategoriId: pasarState.pasarDesa.edit.form.kategoriId || [],
})
});
useEffect(() => {
pasarState.kategoriProduk.findMany.load();
@@ -38,184 +50,192 @@ function EditPasarDesa() {
const data = await pasarState.pasarDesa.edit.load(id);
if (data) {
setFormData({
nama: data.nama || "",
nama: data.nama || '',
harga: data.harga || 0,
alamatUsaha: data.alamatUsaha || "",
imageId: data.imageId || "",
alamatUsaha: data.alamatUsaha || '',
imageId: data.imageId || '',
rating: data.rating || 0,
// Use the IDs from KategoriToPasar relationship
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
});
// 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 pasar desa:", error);
console.error('Error loading pasar desa:', error);
toast.error(
error instanceof Error ? error.message : "Gagal mengambil data pasar desa"
error instanceof Error ? error.message : 'Gagal mengambil data pasar desa'
);
}
}
};
loadPasarDesa();
}, [params?.id]);
const handleSubmit = async () => {
try {
pasarState.pasarDesa.edit.form = {
...pasarState.pasarDesa.edit.form,
nama: formData.nama,
harga: formData.harga,
alamatUsaha: formData.alamatUsaha,
imageId: formData.imageId,
rating: formData.rating,
kategoriId: formData.kategoriId,
}
pasarState.pasarDesa.edit.form = { ...pasarState.pasarDesa.edit.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
pasarState.pasarDesa.edit.form.imageId = uploaded.id;
}
await pasarState.pasarDesa.edit.update();
toast.success("Tips Keamanan berhasil diperbarui!");
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa");
toast.success('Pasar desa berhasil diperbarui!');
router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
} catch (error) {
console.error("Error updating pasar desa:", error);
toast.error("Terjadi kesalahan saat memperbarui pasar desa");
console.error('Error updating pasar desa:', error);
toast.error('Terjadi kesalahan saat memperbarui pasar 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">
Edit Pasar Desa
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Pasar 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"}>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 Produk
</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 Produk"
placeholder="Masukkan nama produk"
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
required
/>
<TextInput
type="number"
label="Harga Produk"
placeholder="Masukkan harga produk"
value={formData.harga}
onChange={(e) => setFormData({ ...formData, harga: Number(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
required
/>
<TextInput
type="number"
min={0}
max={5}
step={0.1} // bisa pakai 0.1 biar support desimal
step={0.1}
label="Rating Produk"
placeholder="Masukkan rating produk (0-5)"
value={formData.rating}
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk (0-5)'
required
/>
<TextInput
label="Alamat Usaha"
placeholder="Masukkan alamat usaha"
value={formData.alamatUsaha}
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
required
/>
<TextInput
value={formData.alamatUsaha}
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<MultiSelect
label="Kategori Produk"
placeholder="Pilih kategori produk"
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val })}
label={<Text fw={"bold"} fz={"sm"}>Kategori Produk</Text>}
placeholder='Pilih kategori produk'
data={
pasarState.kategoriProduk.findMany.data?.map((v) => ({
value: v.id, // Make sure this is using the ID
label: v.nama
value: v.id,
label: v.nama,
})) || []
}
clearable
searchable
required
error={!formData.kategoriId.length ? "Pilih minimal satu kategori" : undefined}
error={!formData.kategoriId.length ? 'Pilih minimal satu kategori' : undefined}
/>
<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>

View File

@@ -1,7 +1,7 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core';
import { IconArrowBack, IconTrash, IconEdit } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -10,119 +10,150 @@ import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function DetailPasarDesa() {
const statePasar = useProxy(pasarDesaState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const statePasar = useProxy(pasarDesaState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
statePasar.pasarDesa.findUnique.load(params?.id as string)
}, [])
statePasar.pasarDesa.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
statePasar.pasarDesa.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa")
statePasar.pasarDesa.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa");
}
}
};
if (!statePasar.pasarDesa.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = statePasar.pasarDesa.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 Pasar Desa</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 Pasar Desa
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Produk</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.nama}</Text>
<Text fz="lg" fw="bold">Nama Produk</Text>
<Text fz="md" c="dimmed">{data.nama || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Harga Produk</Text>
<Text fz={"lg"}>Rp.{statePasar.pasarDesa.findUnique.data?.harga}</Text>
<Text fz="lg" fw="bold">Harga Produk</Text>
<Text fz="md" c="dimmed">Rp. {data.harga || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Rating Produk</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.rating}</Text>
<Text fz="lg" fw="bold">Rating Produk</Text>
<Text fz="md" c="dimmed">{data.rating || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat Usaha</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.alamatUsaha}</Text>
<Text fz="lg" fw="bold">Alamat Usaha</Text>
<Text fz="md" c="dimmed">{data.alamatUsaha || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={statePasar.pasarDesa.findUnique.data?.image?.link} alt="gambar" />
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.nama || 'Gambar Produk'}
w={120}
h={120}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
<Text fz="lg" fw="bold">Kategori</Text>
<Stack gap={4}>
{statePasar.pasarDesa.findUnique.data?.KategoriToPasar &&
statePasar.pasarDesa.findUnique.data.KategoriToPasar.length > 0 ? (
statePasar.pasarDesa.findUnique.data.KategoriToPasar.map((kategori) => (
<Text fz={"lg"} key={kategori.id}>
{data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
data.KategoriToPasar.map((kategori) => (
<Text fz="md" c="dimmed" key={kategori.id}>
{kategori.kategori.nama}
</Text>
))
) : (
<Text fz={"lg"} c="dimmed">
Tidak ada kategori
</Text>
<Text fz="sm" c="dimmed">Tidak ada kategori</Text>
)}
</Stack>
</Box>
<Box>
<Flex gap={"xs"}>
<Group gap="sm">
<Tooltip label="Hapus Produk" withArrow position="top">
<Button
color="red"
onClick={() => {
if (statePasar.pasarDesa.findUnique.data) {
setSelectedId(statePasar.pasarDesa.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={!statePasar.pasarDesa.findUnique.data}
color="red">
<IconX size={20} />
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Produk" withArrow position="top">
<Button
onClick={() => {
if (statePasar.pasarDesa.findUnique.data) {
router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${statePasar.pasarDesa.findUnique.data.id}/edit`);
}
}}
disabled={!statePasar.pasarDesa.findUnique.data}
color="green">
color="green"
onClick={() => router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${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 produk ini?"
text="Apakah Anda yakin ingin menghapus produk ini?"
/>
</Box>
);

View File

@@ -1,8 +1,20 @@
/* 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, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
MultiSelect,
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 +23,9 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
function CreatePasarDesa() {
export default function CreatePasarDesa() {
const router = useRouter();
const statePasar = useProxy(pasarDesaState)
const statePasar = useProxy(pasarDesaState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
@@ -23,170 +35,185 @@ function CreatePasarDesa() {
const resetForm = () => {
statePasar.pasarDesa.create.form = {
nama: "",
nama: '',
harga: 0,
alamatUsaha: "",
imageId: "",
alamatUsaha: '',
imageId: '',
rating: 0,
kategoriId: [],
};
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');
}
statePasar.pasarDesa.create.form.imageId = uploaded.id;
await statePasar.pasarDesa.create.create();
resetForm();
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa")
}
router.push('/admin/ekonomi/pasar-desa/produk-pasar-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">
{/* Header dengan tombol kembali */}
<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 Produk Pasar Desa
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Pasar Desa</Title>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Upload Gambar */}
<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 Produk
</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>
{/* Nama Produk */}
<TextInput
label="Nama Produk"
placeholder="Masukkan nama produk"
value={statePasar.pasarDesa.create.form.nama}
onChange={(val) => {
statePasar.pasarDesa.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
onChange={(e) => (statePasar.pasarDesa.create.form.nama = e.target.value)}
required
/>
{/* Harga Produk */}
<TextInput
type="number"
label="Harga Produk"
placeholder="Masukkan harga produk"
value={statePasar.pasarDesa.create.form.harga}
onChange={(val) => {
statePasar.pasarDesa.create.form.harga = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
onChange={(e) => (statePasar.pasarDesa.create.form.harga = Number(e.target.value))}
required
/>
{/* Rating */}
<TextInput
type="number"
min={0}
max={5}
step={0.1} // bisa pakai 0.1 biar support desimal
step={0.1}
label="Rating Produk (05)"
placeholder="Masukkan rating produk"
value={statePasar.pasarDesa.create.form.rating}
onChange={(val) => {
const value = Number(val.target.value);
// Validasi manual juga boleh (jaga-jaga)
onChange={(e) => {
const value = Number(e.target.value);
if (value >= 0 && value <= 5) {
statePasar.pasarDesa.create.form.rating = value;
}
}}
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk (0-5)'
/>
{/* Alamat Usaha */}
<TextInput
label="Alamat Usaha"
placeholder="Masukkan alamat usaha"
value={statePasar.pasarDesa.create.form.alamatUsaha}
onChange={(val) => {
statePasar.pasarDesa.create.form.alamatUsaha = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
onChange={(e) => (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)}
/>
{/* Kategori Produk */}
<MultiSelect
label="Kategori Produk"
placeholder="Pilih kategori produk"
value={statePasar.pasarDesa.create.form.kategoriId}
onChange={(val) => {
statePasar.pasarDesa.create.form.kategoriId = val;
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori Produk</Text>}
placeholder='Pilih kategori produk'
onChange={(val) => (statePasar.pasarDesa.create.form.kategoriId = val)}
data={
statePasar.kategoriProduk.findMany.data?.map((v) => ({
value: v.id,
label: v.nama
label: v.nama,
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
{/* Tombol Submit */}
<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 CreatePasarDesa;

View File

@@ -1,22 +1,39 @@
'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 { 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 { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
import { useState } from 'react';
function PasarDesa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Produk Pasar Desa'
placeholder='pencarian'
title="Produk Pasar Desa"
placeholder="Cari produk pasar desa..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,71 +44,119 @@ function PasarDesa() {
}
function ListPasarDesa({ search }: { search: string }) {
const statePasar = useProxy(pasarDesaState.pasarDesa)
const statePasar = useProxy(pasarDesaState.pasarDesa);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = statePasar.findMany
const { data, page, totalPages, loading, load } = statePasar.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
load(page, 10, search);
}, [page, search]);
const filteredData = 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 Produk Pasar Desa'
href='/admin/ekonomi/pasar-desa/produk-pasar-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Produk</TableTh>
<TableTh>Harga Produk</TableTh>
<TableTh>Rating Produk</TableTh>
<TableTh>Alamat Usaha</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>Rp.{item.harga}</TableTd>
<TableTd>{item.rating}</TableTd>
<TableTd>{item.alamatUsaha}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Produk Pasar Desa</Title>
<Tooltip label="Tambah Produk Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama Produk</TableTh>
<TableTh style={{ width: '20%' }}>Harga Produk</TableTh>
<TableTh style={{ width: '15%' }}>Rating</TableTh>
<TableTh style={{ width: '25%' }}>Alamat Usaha</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</TableTd>
<TableTd>
<Text>Rp.{item.harga}</Text>
</TableTd>
<TableTd>
<Text>{item.rating || '-'}</Text>
</TableTd>
<TableTd>
<Text truncate fz="sm" c="dimmed">
{item.alamatUsaha || '-'}
</Text>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/ekonomi/pasar-desa/produk-pasar-desa/${item.id}`
)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada produk pasar desa yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
my={"md"}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -2,6 +2,8 @@
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
import colors from '@/con/colors';
import {
@@ -13,11 +15,13 @@ import {
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function EditProgramKemiskinan() {
const router = useRouter();
@@ -33,46 +37,74 @@ function EditProgramKemiskinan() {
stateProgram.update.form = {
nama: data.nama || '',
deskripsi: data.deskripsi || '',
ikonUrl: data.ikonUrl || '',
icon: data.icon || '',
statistik: {
tahun: data.statistik?.tahun?.toString() || '',
jumlah: data.statistik?.jumlah?.toString() || '',
},
};
}
}).catch((err) => {
console.error("Error load data:", err);
toast.error("Gagal mengambil data program");
});
}
}, [id]);
const handleSubmit = async () => {
stateProgram.update.id = id;
await stateProgram.update.update();
router.push('/admin/ekonomi/program-kemiskinan');
try {
stateProgram.update.id = id;
await stateProgram.update.update();
toast.success("Program berhasil diperbarui!");
router.push('/admin/ekonomi/program-kemiskinan');
} catch (error) {
console.error("Error update program:", error);
toast.error("Terjadi kesalahan saat memperbarui program");
}
};
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 Program Kemiskinan</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol kembali */}
<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 Kemiskinan
</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
value={stateProgram.update.form.nama}
onChange={(e) => {
stateProgram.update.form.nama = e.target.value;
}}
label={<Text fw="bold" fz="md">Judul Program</Text>}
label={<Text fw="bold" fz="sm">Judul Program</Text>}
placeholder="Masukkan judul program"
required
/>
<Box>
<Text fw="bold" fz="md">Deskripsi</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={stateProgram.update.form.deskripsi}
onChange={(val) => {
@@ -81,40 +113,58 @@ function EditProgramKemiskinan() {
/>
</Box>
<TextInput
value={stateProgram.update.form.ikonUrl}
onChange={(e) => {
stateProgram.update.form.ikonUrl = e.target.value;
}}
label={<Text fw="bold" fz="md">Ikon URL</Text>}
placeholder="Masukkan ikon url"
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Ikon Program Kreatif Desa
</Text>
<SelectIconProgramEdit
value={stateProgram.update.form.icon as IconKey}
onChange={(value) => {
stateProgram.update.form.icon = value;
}}
/>
</Box>
<Text fw="bold" fz="md">Statistik Jumlah Masyarakat Miskin</Text>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Statistik Jumlah Masyarakat Miskin
</Text>
<TextInput
type="number"
value={stateProgram.update.form.statistik.jumlah}
onChange={(e) => {
stateProgram.update.form.statistik.jumlah = e.target.value;
}}
label="Jumlah Masyarakat Miskin"
placeholder="Masukkan jumlah masyarakat miskin"
required
/>
<TextInput
type="number"
value={stateProgram.update.form.statistik.jumlah}
onChange={(e) => {
stateProgram.update.form.statistik.jumlah = e.target.value;
}}
label={<Text fw="bold" fz="md">Jumlah Masyarakat Miskin</Text>}
placeholder="Masukkan jumlah masyarakat miskin"
/>
<TextInput
type="number"
value={stateProgram.update.form.statistik.tahun}
onChange={(e) => {
stateProgram.update.form.statistik.tahun = e.target.value;
}}
label="Tahun"
placeholder="Masukkan tahun"
required
mt="sm"
/>
</Box>
<TextInput
type="number"
value={stateProgram.update.form.statistik.tahun}
onChange={(e) => {
stateProgram.update.form.statistik.tahun = e.target.value;
}}
label={<Text fw="bold" fz="md">Tahun</Text>}
placeholder="Masukkan tahun"
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Submit
<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>

View File

@@ -1,115 +1,169 @@
'use client'
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';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
import { IconKey, IconMapper } from '../../../_com/iconMap';
function DetailProgramKemiskinan() {
const programState = useProxy(programKemiskinanState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const programState = useProxy(programKemiskinanState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const params = useParams()
const params = useParams();
useShallowEffect(() => {
programState.findUnique.load(params?.id as string)
}, [])
programState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
programState.delete.delete(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/program-kemiskinan")
programState.delete.delete(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/ekonomi/program-kemiskinan");
}
}
};
if (!programState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = programState.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 Kemiskinan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card utama */}
<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 Program Kemiskinan
</Text>
{/* Detail Content */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fw={"bold"}>Judul Program</Text>
<Text fz={"md"}>{programState.findUnique.data?.nama}</Text>
<Text fz="lg" fw="bold">Judul Program</Text>
<Text fz="md" c="dimmed">{data.nama || '-'}</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: programState.findUnique.data?.deskripsi }}></Text>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fw={"bold"}>Ikon URL</Text>
<Text fz={"md"}>{programState.findUnique.data?.ikonUrl}</Text>
<Text fz="lg" fw="bold">Ikon Program</Text>
{data.icon ? (
<IconMapper
name={data.icon as IconKey}
size={32}
color={colors['blue-button']}
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada ikon</Text>
)}
</Box>
<Text fw={"bold"}>Statistik Jumlah Masyarakat Miskin</Text>
<Box>
<Text fw={"bold"}>Jumlah Masyarakat Miskin</Text>
<Text fz={"md"}>{programState.findUnique.data?.statistik?.jumlah}</Text>
<Text fz="lg" fw="bold">Statistik Jumlah Masyarakat Miskin</Text>
<Stack gap="xs" pl="sm">
<Box>
<Text fw="bold">Jumlah</Text>
<Text fz="md" c="dimmed">{data.statistik?.jumlah || '-'}</Text>
</Box>
<Box>
<Text fw="bold">Tahun</Text>
<Text fz="md" c="dimmed">{data.statistik?.tahun || '-'}</Text>
</Box>
</Stack>
</Box>
<Box>
<Text fw={"bold"}>Tahun</Text>
<Text fz={"md"}>{programState.findUnique.data?.statistik?.tahun}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (programState.findUnique.data) {
setSelectedId(programState.findUnique.data.id);
{/* Action Buttons */}
<Group gap="sm" mt="sm">
<Tooltip label="Hapus Program" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}
}}
disabled={programState.delete.loading || !programState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (programState.findUnique.data) {
router.push(`/admin/ekonomi/program-kemiskinan/${programState.findUnique.data.id}/edit`);
}
}}
disabled={!programState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
}}
variant="light"
radius="md"
size="md"
disabled={programState.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Program" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kemiskinan ini?"
text="Apakah Anda yakin ingin menghapus program kemiskinan ini?"
/>
</Box>
);
}
export default DetailProgramKemiskinan;

View File

@@ -1,34 +1,50 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
'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,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
import CreateEditor from '../../../_com/createEditor';
import SelectIconProgram from '../../../_com/selectIcon';
import { useState } from 'react';
import { toast } from 'react-toastify';
function CreateProgramKemiskinan() {
const programState = useProxy(programKemiskinanState)
const programState = useProxy(programKemiskinanState);
const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]);
const resetForm = () => {
programState.create.form = {
nama: "",
deskripsi: "",
ikonUrl: "",
nama: '',
deskripsi: '',
icon: '',
statistik: {
tahun: "",
jumlah: "",
}
}
}
tahun: '',
jumlah: '',
},
};
};
const handleSubmit = async () => {
if (!programState.create.form.nama || !programState.create.form.deskripsi) {
return toast.warn('Judul dan deskripsi wajib diisi');
}
const id = await programState.create.create();
if (id) {
const idStr = String(id);
@@ -36,68 +52,116 @@ function CreateProgramKemiskinan() {
if (programState.findUnique.data) {
setLineChart([programState.findUnique.data]);
}
toast.success('Program berhasil ditambahkan');
} else {
toast.error('Gagal menambahkan program, coba lagi');
}
resetForm()
router.push("/admin/ekonomi/program-kemiskinan")
}
resetForm();
router.push('/admin/ekonomi/program-kemiskinan');
};
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">
{/* Header dengan tombol back */}
<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 Kemiskinan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Program Kemiskinan</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">
{/* Judul Program */}
<TextInput
label="Judul Program"
placeholder="Masukkan judul program"
value={programState.create.form.nama}
onChange={(val) => {
programState.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Judul Program</Text>}
placeholder='Masukkan judul program'
onChange={(val) => (programState.create.form.nama = val.target.value)}
required
/>
<Box>
<Text fw={"bold"} fz={"md"}>Deskripsi</Text>
<CreateEditor
value={programState.create.form.deskripsi}
onChange={(val) => {
programState.create.form.deskripsi = val;
}}
/>
</Box>
<TextInput
value={programState.create.form.ikonUrl}
onChange={(val) => {
programState.create.form.ikonUrl = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Ikon URL</Text>}
placeholder='Masukkan ikon url'
/>
<Text fw={"bold"} fz={"md"}>Statistik Jumlah Masyarakat Miskin</Text>
<TextInput
type='number'
value={programState.create.form.statistik.jumlah}
onChange={(val) => {
programState.create.form.statistik.jumlah = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Jumlah Masyarakat Miskin</Text>}
placeholder='Masukkan jumlah masyarakat miskin'
/>
<TextInput
type='number'
value={programState.create.form.statistik.tahun}
onChange={(val) => {
programState.create.form.statistik.tahun = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Tahun</Text>}
placeholder='Masukkan tahun'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
{/* Ikon Program */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Ikon Program Kreatif Desa
</Text>
<SelectIconProgram
onChange={(value) => (programState.create.form.icon = value)}
/>
</Box>
{/* Deskripsi */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={programState.create.form.deskripsi}
onChange={(val) => {
programState.create.form.deskripsi = val;
}}
/>
</Box>
{/* Statistik */}
<Text fw="bold" fz="sm">
Statistik Jumlah Masyarakat Miskin
</Text>
<Group grow>
<TextInput
type="number"
value={programState.create.form.statistik.jumlah}
onChange={(val) =>
(programState.create.form.statistik.jumlah = val.target.value)
}
label="Jumlah"
placeholder="Masukkan jumlah masyarakat miskin"
required
/>
<TextInput
type="number"
value={programState.create.form.statistik.tahun}
onChange={(val) =>
(programState.create.form.statistik.tahun = val.target.value)
}
label="Tahun"
placeholder="Masukkan tahun"
required
/>
</Group>
{/* Tombol Submit */}
<Group justify="right" mt="sm">
<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>

View File

@@ -1,16 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan';
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 { useEffect, useState } from 'react';
import { CartesianGrid, Legend, Line, LineChart, Tooltip, XAxis, YAxis } from 'recharts';
import { CartesianGrid, Legend, Line, LineChart, Tooltip as RechartTooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan';
function ProgramKemiskinan() {
const [search, setSearch] = useState("");
@@ -18,7 +17,7 @@ function ProgramKemiskinan() {
<Box>
<HeaderSearch
title='Program Kemiskinan'
placeholder='pencarian'
placeholder='Cari judul program atau deskripsi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,23 +28,17 @@ function ProgramKemiskinan() {
}
function ListProgramKemiskinan({ search }: { search: string }) {
const programState = useProxy(programKemiskinanState)
const programState = useProxy(programKemiskinanState);
const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const {
data,
page,
totalPages,
loading,
load,
} = programState.findMany;
const { data, page, totalPages, loading, load } = programState.findMany;
useShallowEffect(() => {
setMounted(true)
load(page, 10, search)
}, [])
setMounted(true);
load(page, 10, search);
}, [page, search]);
useEffect(() => {
if (data) {
@@ -55,103 +48,118 @@ function ListProgramKemiskinan({ search }: { search: string }) {
tahun: item.statistik?.tahun,
jumlah: Number(item.statistik?.jumlah)
}))
.sort((a, b) => (a.tahun || 0) - (b.tahun || 0)); // opsional, urutkan tahun
.sort((a, b) => (a.tahun || 0) - (b.tahun || 0));
setLineChart(chartData);
}
}, [data])
}, [data]);
const filteredData = 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 Program Kemiskinan'
href='/admin/ekonomi/program-kemiskinan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Judul Program</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Jumlah Masyarakat Miskin</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Text fz={'sm'} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>{item.statistik?.jumlah}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Program Kemiskinan</Title>
<Tooltip label="Tambah Program Kemiskinan" withArrow>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/program-kemiskinan/create')}>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Judul Program</TableTh>
<TableTh style={{ width: '40%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '20%' }}>Jumlah Masyarakat Miskin</TableTh>
<TableTh style={{ width: '10%' }}>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map(item => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd>
<Text fz="sm" truncate lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>{item.statistik?.jumlah || '-'}</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${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 program kemiskinan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<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>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'} >
<Stack>
<Box >
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
{mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}>
<Box w={"100%"} style={{ overflowX: 'auto' }}>
<LineChart
width={820}
height={300}
data={lineChart}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="tahun" />
<YAxis />
<Tooltip
formatter={(value: any, name: string) => [`${value} orang`, name]}
labelFormatter={(label: any) => `Tahun: ${label}`}
/>
<Legend />
<Line
type="monotone"
dataKey="jumlah"
name="Jumlah per Tahun"
stroke={colors['blue-button']}
/>
</LineChart>
</Box>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
{mounted && lineChart.length > 0 ? (
<Box style={{ width: '100%', overflowX: 'auto' }}>
<LineChart width={820} height={300} data={lineChart}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="tahun" />
<YAxis />
<RechartTooltip
formatter={(value: any, name: string) => [`${value} orang`, name]}
labelFormatter={(label: any) => `Tahun: ${label}`}
/>
<Legend />
<Line type="monotone" dataKey="jumlah" name="Jumlah per Tahun" stroke={colors['blue-button']} />
</LineChart>
</Box>
</Stack>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
</Box>
</Box>
);

View File

@@ -3,87 +3,138 @@
import grafikSektorUnggulan from '@/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa';
import colors from '@/con/colors';
import { Box, Button, 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 { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
function EditSektorUnggulanDesa() {
const router = useRouter()
const params = useParams() as { id: string }
const stateGrafik = useProxy(grafikSektorUnggulan)
const router = useRouter();
const params = useParams() as { id: string };
const stateGrafik = useProxy(grafikSektorUnggulan);
const id = params.id
const id = params.id;
// Load data saat komponen mount
useEffect(() => {
if (id) {
stateGrafik.findUnique.load(id).then(() => {
const data = stateGrafik.findUnique.data
const data = stateGrafik.findUnique.data;
if (data) {
stateGrafik.update.form = {
name: data.name || '',
description: data.description || '',
value: data.value || 0,
}
};
}
})
}).catch((err) => {
console.error('Error load sektor unggulan:', err);
toast.error('Gagal mengambil data sektor unggulan');
});
}
}, [id])
}, [id]);
const handleSubmit = async () => {
// Set the ID before submitting
stateGrafik.update.id = id;
await stateGrafik.update.submit();
router.push('/admin/ekonomi/sektor-unggulan-desa')
}
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 Sektor Unggulan Desa</Title>
try {
stateGrafik.update.id = id;
await stateGrafik.update.submit();
toast.success('Sektor unggulan berhasil diperbarui!');
router.push('/admin/ekonomi/sektor-unggulan-desa');
} catch (error) {
console.error('Error update sektor unggulan:', error);
toast.error('Terjadi kesalahan saat memperbarui sektor unggulan');
}
};
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={22} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Sektor Unggulan 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">
<TextInput
label="Nama Sektor Unggulan"
placeholder="masukkan nama sektor unggulan"
placeholder="Masukkan nama sektor unggulan"
value={stateGrafik.update.form.name}
onChange={(val) => {
stateGrafik.update.form.name = val.currentTarget.value;
}}
required
/>
<TextInput
label="Deskripsi Sektor Unggulan"
placeholder="masukkan deskripsi sektor unggulan"
value={stateGrafik.update.form.description}
onChange={(val) => {
stateGrafik.update.form.description = val.currentTarget.value;
}}
/>
<Box>
<Text fz="sm" fw="bold">
Konten
</Text>
<EditEditor
value={stateGrafik.update.form.description}
onChange={(htmlContent) => {
stateGrafik.update.form.description = htmlContent;
}}
/>
</Box>
<TextInput
label="Jumlah"
type="number"
placeholder="masukkan jumlah"
placeholder="Masukkan jumlah"
value={stateGrafik.update.form.value}
onChange={(val) => {
stateGrafik.update.form.value = Number(val.currentTarget.value);
}}
required
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan
</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 EditSektorUnggulanDesa;

View File

@@ -1,97 +1,134 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
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';
import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
function DetailSektorUnggulanDesa() {
const stateGrafik = useProxy(grafikSektorUnggulan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const stateGrafik = useProxy(grafikSektorUnggulan);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
stateGrafik.findUnique.load(params?.id as string)
}, [])
stateGrafik.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
stateGrafik.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/sektor-unggulan-desa")
stateGrafik.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push('/admin/ekonomi/sektor-unggulan-desa');
}
}
};
if (!stateGrafik.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = stateGrafik.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 Sektor Unggulan Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box py={10}>
{/* Tombol kembali */}
<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 Sektor Unggulan Desa
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Sektor Unggulan</Text>
<Text fz={"lg"}>{stateGrafik.findUnique.data?.name}</Text>
<Text fz="lg" fw="bold">Nama Sektor Unggulan</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Sektor Unggulan</Text>
<Text fz={"lg"}>{stateGrafik.findUnique.data?.description}</Text>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text ta={"justify"} fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.description || '-' }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jumlah Sektor Unggulan</Text>
<Text fz={"lg"}>{stateGrafik.findUnique.data?.value}</Text>
<Text fz="lg" fw="bold">Jumlah</Text>
<Text fz="md" c="dimmed">{data.value ?? '-'}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
{/* Tombol Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Sektor Unggulan" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateGrafik.findUnique.data) {
setSelectedId(stateGrafik.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={!stateGrafik.findUnique.data}
color="red">
<IconX size={20} />
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Sektor Unggulan" withArrow position="top">
<Button
onClick={() => {
if (stateGrafik.findUnique.data) {
router.push(`/admin/ekonomi/sektor-unggulan-desa/${stateGrafik.findUnique.data.id}/edit`);
}
}}
disabled={!stateGrafik.findUnique.data}
color="green">
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/sektor-unggulan-desa/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}

View File

@@ -1,8 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
'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,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -13,15 +23,15 @@ import CreateEditor from '../../../_com/createEditor';
function CreateSektorUnggulanDesa() {
const stateGrafik = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter()
const router = useRouter();
const resetForm = () => {
stateGrafik.create.form = {
name: "",
description: "",
name: '',
description: '',
value: 0,
}
}
};
};
const handleSubmit = async () => {
const id = await stateGrafik.create.create();
@@ -33,58 +43,87 @@ function CreateSektorUnggulanDesa() {
}
}
resetForm();
router.push("/admin/ekonomi/sektor-unggulan-desa");
}
router.push('/admin/ekonomi/sektor-unggulan-desa');
};
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 Grafik Hasil Kepuasan Masyarakat</Title>
<Stack gap={"xs"}>
<TextInput
label="Nama Sektor Unggulan"
type="text"
value={stateGrafik.create.form.name}
placeholder="Masukkan nama sektor unggulan"
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */}
<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 Sektor Unggulan Desa
</Title>
</Group>
{/* Form */}
<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 Sektor Unggulan"
placeholder="Masukkan nama sektor unggulan"
value={stateGrafik.create.form.name}
onChange={(e) => {
stateGrafik.create.form.name = e.currentTarget.value;
}}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Sektor Unggulan
</Text>
<CreateEditor
value={stateGrafik.create.form.description}
onChange={(val) => {
stateGrafik.create.form.name = val.currentTarget.value;
stateGrafik.create.form.description = val;
}}
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Sektor Ungggulan</Text>
<CreateEditor
value={stateGrafik.create.form.description}
onChange={(val) => {
stateGrafik.create.form.description = val;
}}
/>
</Box>
<TextInput
label="Jumlah"
type="number"
value={stateGrafik.create.form.value}
placeholder="Masukkan jumlah"
onChange={(val) => {
stateGrafik.create.form.value = Number(val.currentTarget.value);
</Box>
<TextInput
label="Jumlah"
type="number"
placeholder="Masukkan jumlah"
value={stateGrafik.create.form.value}
onChange={(e) => {
stateGrafik.create.form.value = Number(e.currentTarget.value);
}}
required
/>
<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)',
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,23 +1,40 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, 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 JudulList from '../../_com/judulList';
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 { useEffect, useState } from 'react';
import grafikSektorUnggulan from '../../_state/ekonomi/sektor-unggulan-desa';
import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip as ReTooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import HeaderSearch from '../../_com/header';
import grafikSektorUnggulan from '../../_state/ekonomi/sektor-unggulan-desa';
function SektorUnggulanDesa() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Sektor Unggulan Desa'
placeholder='pencarian'
title="Sektor Unggulan Desa"
placeholder="Cari nama sektor atau nilai..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -30,96 +47,155 @@ function SektorUnggulanDesa() {
function ListSektorUnggulanDesa({ search }: { search: string }) {
const router = useRouter();
const state = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<{ id: string; name: string; description: string | null; value: number | null }[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const [chartData, setChartData] = useState<
{ id: string; name: string; description: string | null; value: number | null }[]
>([]);
const filteredData = (state.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
(item.value?.toString() || '').toLowerCase().includes(keyword)
);
});
useShallowEffect(() => {
setMounted(true)
state.findMany.load()
}, [])
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useEffect(() => {
setMounted(true);
if (state.findMany.data) {
setChartData(state.findMany.data.map((item) => ({
id: item.id,
name: item.name,
description: item.description,
value: Number(item.value),
})));
setChartData(
state.findMany.data.map((item) => ({
id: item.id,
name: item.name,
description: item.description,
value: Number(item.value),
}))
);
}
}, [state.findMany.data]);
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Sektor Unggulan Desa'
href='/admin/ekonomi/sektor-unggulan-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Sektor Unggulan</TableTh>
<TableTh>Deskripsi Sektor Unggulan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate={"end"} fz={'sm'} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '' }}></Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
{/* 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 Hasil Kepuasan Masyarakat</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box>
const filteredData = data || []
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Stack gap="md" py="md">
{/* List Table */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Sektor Unggulan Desa</Title>
<Tooltip label="Tambah Sektor Unggulan Desa" withArrow>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/sektor-unggulan-desa/create')}>
Tambah Baru
</Button>
</Tooltip>
</Group>
{loading ? (
<Skeleton height={300} radius="md" />
) : (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Sektor Unggulan Desa</Title>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="value" fill={colors['blue-button']} name="Jumlah" />
</BarChart>
)}
</Paper>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama Sektor</TableTh>
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }} />
</Box>
</TableTd>
<TableTd>
<Tooltip label="Lihat detail sektor" withArrow>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={6}>Detail</Text>
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data sektor unggulan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
)}
</Stack>
</Box>
</Paper>
<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>
{/* Chart */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Title order={4} pb="sm">
Grafik Sektor Unggulan Desa
</Title>
{loading ? (
<Skeleton height={350} radius="md" />
) : chartData.length > 0 ? (
<Box style={{ width: '100%', height: 400 }}>
<ResponsiveContainer>
<BarChart data={chartData}>
<XAxis dataKey="name" />
<YAxis />
<ReTooltip />
<Legend />
<Bar dataKey="value" fill={colors['blue-button']} name="Jumlah" />
</BarChart>
</ResponsiveContainer>
</Box>
) : (
<Center py={50}>
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
</Center>
)}
</Paper>
</Stack>
);
}

View File

@@ -1,67 +1,142 @@
/* 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,
ScrollArea,
} from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import {
IconHierarchy,
IconUsers,
IconGitBranch,
} 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: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi"
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi",
icon: <IconHierarchy size={18} stroke={1.8} />,
tooltip: "Kelola daftar posisi organisasi",
},
{
label: "Pegawai",
value: "pegawai",
href: "/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai"
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Kelola data pegawai BUMDesa",
},
{
label: "Hubungan Organisasi",
value: "hubunganorganisasi",
href: "/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/hubungan-organisasi"
href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi",
icon: <IconGitBranch size={18} stroke={1.8} />,
tooltip: "Atur hubungan antar posisi organisasi",
},
];
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}>Struktur Organisasi & SK Pengurus BUMDesa</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 */}
<></>
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Struktur Organisasi & SK Pengurus BUMDesa
</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal biar rapi kalau label panjang */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{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",
flexShrink: 0,
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{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;

View File

@@ -126,7 +126,7 @@ export default function EditPegawai() {
const success = await stateOrganisasi.edit.submit();
if (success) {
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai");
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);

View File

@@ -25,7 +25,7 @@ function DetailPegawai() {
statePegawai.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai")
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai")
}
}
@@ -120,7 +120,7 @@ function DetailPegawai() {
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
}
}}
disabled={!statePegawai.findUnique.data}

View File

@@ -64,7 +64,7 @@ function CreatePegawai() {
// Reset form dan redirect
resetForm();
toast.success("Data pegawai berhasil ditambahkan");
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai");
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
} catch (error) {
console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");

View File

@@ -78,7 +78,7 @@ function ListPegawai({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
<JudulList
title='List Pegawai'
href='/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/create'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
@@ -131,7 +131,7 @@ function ListPegawai({ search }: { search: string }) {
</Group>
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/${item.id}`)}>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>

View File

@@ -3,7 +3,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
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 { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -17,8 +27,8 @@ function EditPosisiOrganisasi() {
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
const [formData, setFormData] = useState({
nama: "",
deskripsi: "",
nama: '',
deskripsi: '',
hierarki: 0,
});
@@ -30,7 +40,6 @@ function EditPosisiOrganisasi() {
const data = await stateOrganisasi.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
stateOrganisasi.edit.id = id;
setFormData({
nama: data.nama || '',
@@ -39,8 +48,8 @@ function EditPosisiOrganisasi() {
});
}
} catch (error) {
console.error("Error loading posisi organisasi:", error);
toast.error("Gagal memuat data posisi organisasi");
console.error('Error loading posisi organisasi:', error);
toast.error('Gagal memuat data posisi organisasi');
}
};
@@ -60,7 +69,6 @@ function EditPosisiOrganisasi() {
hierarki: formData.hierarki,
};
// Safety check tambahan: pastikan ID tidak kosong
if (!stateOrganisasi.edit.id) {
stateOrganisasi.edit.id = id; // fallback
}
@@ -68,45 +76,92 @@ function EditPosisiOrganisasi() {
const success = await stateOrganisasi.edit.update();
if (success) {
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi");
toast.success('Posisi organisasi berhasil diperbarui!');
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'
);
}
} catch (error) {
console.error("Error updating posisi organisasi:", error);
// toast akan ditampilkan dari fungsi update
console.error('Error updating posisi organisasi:', error);
}
};
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 Posisi Organisasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Posisi Organisasi</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
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Posisi Organisasi</Text>}
placeholder='Masukkan nama posisi organisasi'
/>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
}}
onChange={(e) =>
setFormData({ ...formData, nama: e.target.value })
}
label="Nama Posisi Organisasi"
placeholder="Masukkan nama posisi organisasi"
required
/>
<Box>
<Text fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData({ ...formData, deskripsi: htmlContent })
}
/>
</Box>
<TextInput
type="number"
value={formData.hierarki}
onChange={(e) => setFormData({ ...formData, hierarki: parseInt(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Hierarki</Text>}
placeholder='Masukkan hierarki'
onChange={(e) =>
setFormData({
...formData,
hierarki: parseInt(e.target.value) || 0,
})
}
label="Hierarki"
placeholder="Masukkan hierarki"
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>

View File

@@ -1,9 +1,19 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import { Box, Button, 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 { useEffect } from 'react';
@@ -11,47 +21,75 @@ import { useProxy } from 'valtio/utils';
function CreatePosisiOrganisasi() {
const router = useRouter();
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi)
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
useEffect(() => {
stateOrganisasi.findMany.load();
}, []);
const resetForm = () => {
stateOrganisasi.create.form = {
nama: "",
deskripsi: "",
hierarki: 0, // Initialize as 0 to allow any number input
nama: '',
deskripsi: '',
hierarki: 0,
};
};
const handleSubmit = async () => {
await stateOrganisasi.create.submit();
resetForm();
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi")
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'
);
};
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"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Posisi Organisasi</Title>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header Back + Title */}
<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 Posisi Organisasi
</Title>
</Group>
{/* Form Card */}
<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 Posisi"
placeholder="Contoh: Kepala Desa"
value={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
required
/>
<CreateEditor
value={stateOrganisasi.create.form.deskripsi}
onChange={(htmlContent) => {
stateOrganisasi.create.form.deskripsi = htmlContent;
}}
/>
<Box>
<Text fz={"sm"} mb={6}>Deskripsi</Text>
<CreateEditor
value={stateOrganisasi.create.form.deskripsi}
onChange={(htmlContent) => {
stateOrganisasi.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<TextInput
label="Hierarki"
type="number"
@@ -63,13 +101,24 @@ function CreatePosisiOrganisasi() {
stateOrganisasi.create.form.hierarki = value;
}
}}
required
/>
<Button
onClick={handleSubmit}
color="blue"
>
Simpan
</Button>
{/* Action 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>

View File

@@ -1,13 +1,30 @@
/* 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 } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import {
Box,
Button,
Center,
Group,
Paper,
Pagination,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useShallowEffect } from '@mantine/hooks';
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 strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
@@ -15,108 +32,161 @@ function PosisiOrganisasi() {
const [search, setSearch] = useState("");
return (
<Box>
{/* Search Bar */}
<HeaderSearch
title='Posisi Organisasi'
placeholder='pencarian'
title="Posisi Organisasi"
placeholder="Cari nama, deskripsi, atau hierarki..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
{/* List Table */}
<ListPosisiOrganisasi search={search} />
</Box>
);
}
function ListPosisiOrganisasi({ search }: { search: string }) {
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi)
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useEffect(() => {
stateOrganisasi.findMany.load()
}, [])
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
const filteredData = data || [];
const handleHapus = async () => {
if (selectedId) {
await stateOrganisasi.delete.byId(selectedId);
setModalHapus(false)
setSelectedId(null)
setModalHapus(false);
setSelectedId(null);
load(page, 10, search); // refresh
}
}
};
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword) ||
item.hierarki?.toString().toLowerCase().includes(keyword)
);
});
if (!stateOrganisasi.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Posisi Organisasi'
href='/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Posisi</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Hierarki</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Text truncate dangerouslySetInnerHTML={{ __html: item.deskripsi ?? "" }} />
</TableTd>
<TableTd>{item.hierarki}</TableTd>
<TableTd>
<Button color="green"
onClick={() => {
if (item) {
router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi/${item.id}`);
}
}}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red"
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={!item}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Posisi Organisasi</Title>
<Tooltip label="Tambah Posisi Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
>
<IconTrash size={20} />
</Button>
</TableTd>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '15%' }}>Hierarki</TableTh>
<TableTh style={{ width: '25%' }}>Edit</TableTh>
<TableTh style={{ width: '25%' }}>Hapus</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Text fz="sm">{item.hierarki ?? '-'}</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Tooltip label="Ubah Posisi Organisasi" withArrow>
<Button
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`
)
}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Tooltip label="Hapus Posisi Organisasi" withArrow>
<Button
variant="light"
color="red"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data posisi organisasi yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<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>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}

View File

@@ -10,6 +10,7 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import SelectIconProgramEdit from '../../../../_com/selectIconEdit';
import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
interface FormProgramKreatif {
name: string;
@@ -18,8 +19,6 @@ interface FormProgramKreatif {
icon: string;
}
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah';
function EditProgramKreatifDesa() {
const stateProgramKreatif = useProxy(programKreatifState)

View File

@@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconChartLine, IconChristmasTreeFilled, IconClipboard, IconEdit, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconShieldFilled, IconTent, IconTrash, IconTrendingUp, IconTrophy, IconTruck, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
import { IconKey, IconMapper } from '../../../_com/iconMap';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
@@ -19,22 +19,6 @@ function DetailProgramKreatifDesa() {
const params = useParams()
const [selectedId, setSelectedId] = useState<string | null>(null)
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
};
useShallowEffect(() => {
stateProgramKreatif.findUnique.load(params?.id as string)
}, [params?.id])
@@ -75,10 +59,12 @@ function DetailProgramKreatifDesa() {
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
{iconMap[stateProgramKreatif.findUnique.data?.icon] && (
<Box title={stateProgramKreatif.findUnique.data?.icon}>
{React.createElement(iconMap[stateProgramKreatif.findUnique.data?.icon], { size: 24 })}
</Box>
{stateProgramKreatif.findUnique.data?.icon && (
<IconMapper
name={stateProgramKreatif.findUnique.data?.icon as IconKey}
size={32}
color={colors['blue-button']}
/>
)}
</Box>
<Box>
@@ -127,7 +113,7 @@ function DetailProgramKreatifDesa() {
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kreatif desa ini?"
/>
/>
</Box>
);
}

View File

@@ -4,7 +4,7 @@
import React from 'react';
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 { IconCash, IconChristmasTreeFilled, IconClipboard, IconDeviceImac, IconDroplet, IconHome, IconHomeEco, IconHospital, IconScale, IconSchool, IconSearch, IconShieldFilled, IconShoppingCart, IconTrash, IconTree, IconTrendingUp, IconTruck } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
@@ -60,6 +60,21 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
rumah: IconHome,
pohon: IconTree,
air: IconDroplet,
bantuan: IconCash,
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital
};
if (loading || !data) {

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconPhone, IconTag } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -51,37 +51,44 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
Kontak Darurat
</Title>
<Tabs
color={colors['blue-button']}
variant='pills'
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>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{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>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel

View File

@@ -4,19 +4,31 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
import colors from '@/con/colors';
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 { DateTimePicker } from '@mantine/dates';
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';
export type Status = "Selesai" | "Proses" | "Gagal";
function EditLaporanPublik() {
const stateLaporan = useProxy(laporanPublikState)
const stateLaporan = useProxy(laporanPublikState);
const router = useRouter();
const params = useParams()
const params = useParams();
const [formData, setFormData] = useState({
judul: stateLaporan.edit.form.judul || '',
@@ -25,7 +37,7 @@ function EditLaporanPublik() {
status: stateLaporan.edit.form.status || '',
penanganan: stateLaporan.edit.form.penanganan || '',
kronologi: stateLaporan.edit.form.kronologi || '',
})
});
useEffect(() => {
const loadLaporanPublik = async () => {
@@ -46,8 +58,9 @@ function EditLaporanPublik() {
}
} catch (error) {
console.error('Error loading laporan publik:', error);
toast.error("Gagal mengambil data laporan publik");
}
}
};
loadLaporanPublik();
}, [params?.id]);
@@ -62,75 +75,88 @@ function EditLaporanPublik() {
status: formData.status,
penanganan: formData.penanganan,
kronologi: formData.kronologi,
}
};
await stateLaporan.edit.update();
toast.success("Laporan Publik berhasil diperbarui!");
router.push("/admin/keamanan/laporan-publik");
} catch (error) {
console.error("Error updating kontak darurat:", error);
toast.error("Terjadi kesalahan saat memperbarui kontak darurat");
console.error("Error updating laporan publik:", error);
toast.error("Terjadi kesalahan saat memperbarui laporan publik");
}
}
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 Laporan Publik</Title>
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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 Laporan Publik
</Title>
</Group>
{/* Card */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Judul Laporan Publik</Text>}
placeholder='Masukkan judul LaporanPublik'
label={<Text fw="bold" fz="sm">Judul Laporan Publik</Text>}
placeholder="Masukkan judul laporan publik"
required
/>
<TextInput
value={formData.lokasi}
onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Lokasi Laporan Publik</Text>}
placeholder='Masukkan lokasi LaporanPublik'
label={<Text fw="bold" fz="sm">Lokasi Laporan Publik</Text>}
placeholder="Masukkan lokasi laporan publik"
required
/>
<DateTimePicker
label="Tanggal Laporan Publik"
value={
formData.tanggalWaktu
? new Date(formData.tanggalWaktu)
: null
label="Tanggal & Waktu Laporan Publik"
value={formData.tanggalWaktu ? new Date(formData.tanggalWaktu) : null}
onChange={(val) =>
setFormData({ ...formData, tanggalWaktu: val ? val.toString() : '' })
}
onChange={(val) => {
if (val) {
setFormData({ ...formData, tanggalWaktu: val.toString() });
} else {
setFormData({ ...formData, tanggalWaktu: "" }); // Reset kalau dikosongkan
}
}}
required
/>
<Select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e?.valueOf() as Status })}
label={<Text fw={"bold"} fz={"sm"}>Status Laporan Publik</Text>}
placeholder='Masukkan status LaporanPublik'
onChange={(e) => setFormData({ ...formData, status: e as Status })}
label={<Text fw="bold" fz="sm">Status Laporan Publik</Text>}
placeholder="Pilih status laporan publik"
data={[
{ value: "Selesai", label: "Selesai" },
{ value: "Proses", label: "Proses" },
{ value: "Gagal", label: "Gagal" },
]}
required
/>
<TextInput
value={formData.kronologi}
onChange={(e) => setFormData({ ...formData, kronologi: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Kronologi Laporan Publik</Text>}
placeholder='Masukkan kronologi LaporanPublik'
label={<Text fw="bold" fz="sm">Kronologi Laporan Publik</Text>}
placeholder="Masukkan kronologi laporan publik"
/>
<Text fw={"bold"} fz={"sm"}>Penanganan Laporan Publik</Text>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Laporan Publik</Text>
<Text fw="bold" fz="sm" mb={6}>Penanganan Laporan Publik</Text>
<EditEditor
value={formData.penanganan}
onChange={(htmlContent) => {
@@ -139,8 +165,20 @@ function EditLaporanPublik() {
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>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>

View File

@@ -1,126 +1,173 @@
'use client'
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 { useProxy } from 'valtio/utils';
import laporanPublikState from '../../../_state/keamanan/laporan-publik';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailLaporanPublik() {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const stateLaporan = useProxy(laporanPublikState)
const params = useParams()
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const stateLaporan = useProxy(laporanPublikState);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
stateLaporan.findUnique.load(params?.id as string)
}, [])
stateLaporan.findUnique.load(params?.id as string);
}, []);
const handleDelete = () => {
if (selectedId) {
stateLaporan.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/keamanan/laporan-publik")
stateLaporan.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push('/admin/keamanan/laporan-publik');
}
}
};
if (!stateLaporan.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = stateLaporan.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 Laporan Publik</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box py={10}>
{/* Tombol Kembali */}
<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 Laporan Publik
</Text>
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz={"lg"} fw={"bold"}>Judul Laporan Publik</Text>
<Text fz={"lg"}>{stateLaporan.findUnique.data?.judul}</Text>
<Text fz="lg" fw="bold">Judul Laporan Publik</Text>
<Text fz="md" c="dimmed">{data.judul || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal Laporan Publik</Text>
<Text fz={"lg"}>
{stateLaporan.findUnique.data?.tanggalWaktu
? new Date(stateLaporan.findUnique.data.tanggalWaktu).toLocaleString('id-ID')
<Text fz="lg" fw="bold">Tanggal Laporan Publik</Text>
<Text fz="md" c="dimmed">
{data.tanggalWaktu
? new Date(data.tanggalWaktu).toLocaleString('id-ID')
: '-'}
</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Lokasi</Text>
<Text fz={"lg"}>{stateLaporan.findUnique.data?.lokasi}</Text>
<Text fz="lg" fw="bold">Lokasi</Text>
<Text fz="md" c="dimmed">{data.lokasi || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Status</Text>
<Text fz={"lg"}>{stateLaporan.findUnique.data?.status}</Text>
<Text fz="lg" fw="bold">Status</Text>
<Text fz="md" c="dimmed">{data.status || '-'}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kronologi</Text>
<Text fz={"lg"}>
{stateLaporan.findUnique.data?.kronologi || '-'}
</Text>
<Text fz="lg" fw="bold">Kronologi</Text>
<Text fz="md" c="dimmed">{data.kronologi || '-'}</Text>
</Box>
<Text fz={"lg"} fw={"bold"}>Penanganan</Text>
{stateLaporan.findUnique.data?.penanganan?.map((item, index) => (
<Box key={index}>
<Text fz={"lg"} fw={"bold"}>Deskripsi Penanganan</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
))}
{!stateLaporan.findUnique.data?.penanganan?.length && (
<Text fz={"lg"} fs="italic">Belum ada penanganan</Text>
)}
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateLaporan.findUnique.data) {
setSelectedId(stateLaporan.findUnique.data.id);
<Box>
<Text fz="lg" fw="bold">Penanganan</Text>
{data.penanganan?.length ? (
data.penanganan.map((item, index) => (
<Box key={index} pl="sm">
<Text fz="md" fw="bold">Deskripsi Penanganan</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
/>
</Box>
))
) : (
<Text fz="sm" fs="italic" c="dimmed">
Belum ada penanganan
</Text>
)}
</Box>
{/* Tombol Aksi */}
<Group gap="sm" mt="sm">
<Tooltip label="Hapus Laporan Publik" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
disabled={stateLaporan.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Laporan Publik" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(`/admin/keamanan/laporan-publik/${data.id}/edit`)
}
}}
disabled={stateLaporan.delete.loading || !stateLaporan.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateLaporan.findUnique.data) {
router.push(`/admin/keamanan/laporan-publik/${stateLaporan.findUnique.data.id}/edit`);
}
}}
disabled={!stateLaporan.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus laporan publik ini?'
/>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus laporan publik ini?"
/>
</Box>
);
}

View File

@@ -1,101 +1,142 @@
'use client'
'use client';
import colors from '@/con/colors';
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 { DateTimePicker } from '@mantine/dates';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import laporanPublikState from '../../../_state/keamanan/laporan-publik';
export type Status = "Selesai" | "Proses" | "Gagal";
export type Status = 'Selesai' | 'Proses' | 'Gagal';
function CreateLaporanPublik() {
const stateLaporan = useProxy(laporanPublikState)
const stateLaporan = useProxy(laporanPublikState);
const router = useRouter();
const resetForm = () => {
stateLaporan.create.form = {
judul: "",
lokasi: "",
tanggalWaktu: "",
status: "Proses" as Status,
penanganan: "",
kronologi: "",
}
}
judul: '',
lokasi: '',
tanggalWaktu: '',
status: 'Proses' as Status,
penanganan: '',
kronologi: '',
};
};
const handleSubmit = async () => {
await stateLaporan.create.create();
resetForm();
router.push('/admin/keamanan/laporan-publik');
}
};
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">
{/* Header with Back Button */}
<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 Laporan Publik
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Laporan Publik</Title>
{/* Form Card */}
<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
value={stateLaporan.create.form.judul}
onChange={(e) => stateLaporan.create.form.judul = e.target.value}
label={<Text fw={"bold"} fz={"sm"}>Judul Laporan Publik</Text>}
placeholder='Masukkan judul LaporanPublik'
onChange={(e) => (stateLaporan.create.form.judul = e.target.value)}
label={<Text fw="bold" fz="sm">Judul Laporan Publik</Text>}
placeholder="Masukkan judul laporan publik"
required
/>
<TextInput
value={stateLaporan.create.form.lokasi}
onChange={(e) => stateLaporan.create.form.lokasi = e.target.value}
label={<Text fw={"bold"} fz={"sm"}>Lokasi Laporan Publik</Text>}
placeholder='Masukkan lokasi LaporanPublik'
onChange={(e) => (stateLaporan.create.form.lokasi = e.target.value)}
label={<Text fw="bold" fz="sm">Lokasi Laporan Publik</Text>}
placeholder="Masukkan lokasi laporan publik"
required
/>
<DateTimePicker
label="Tanggal Laporan Publik"
label={<Text fw="bold" fz="sm">Tanggal Laporan Publik</Text>}
value={
stateLaporan.create.form.tanggalWaktu
? new Date(stateLaporan.create.form.tanggalWaktu)
: null
}
onChange={(val) => {
if (val) {
stateLaporan.create.form.tanggalWaktu = val.toString();
} else {
stateLaporan.create.form.tanggalWaktu = ""; // Reset kalau dikosongkan
}
stateLaporan.create.form.tanggalWaktu = val ? val.toString() : '';
}}
/>
<Select
value={stateLaporan.create.form.status}
onChange={(e) => stateLaporan.create.form.status = e?.valueOf() as Status}
label={<Text fw={"bold"} fz={"sm"}>Status Laporan Publik</Text>}
placeholder='Masukkan status LaporanPublik'
onChange={(e) => (stateLaporan.create.form.status = e?.valueOf() as Status)}
label={<Text fw="bold" fz="sm">Status Laporan Publik</Text>}
placeholder="Pilih status laporan publik"
data={[
{ value: "Selesai", label: "Selesai" },
{ value: "Proses", label: "Proses" },
{ value: "Gagal", label: "Gagal" },
{ value: 'Selesai', label: 'Selesai' },
{ value: 'Proses', label: 'Proses' },
{ value: 'Gagal', label: 'Gagal' },
]}
/>
<TextInput
value={stateLaporan.create.form.kronologi}
onChange={(e) => stateLaporan.create.form.kronologi = e.target.value}
label={<Text fw={"bold"} fz={"sm"}>Kronologi Laporan Publik</Text>}
placeholder='Masukkan kronologi LaporanPublik'
onChange={(e) => (stateLaporan.create.form.kronologi = e.target.value)}
label={<Text fw="bold" fz="sm">Kronologi Laporan Publik</Text>}
placeholder="Masukkan kronologi laporan publik"
required
/>
<Text fw={"bold"} fz={"sm"}>Penanganan Laporan Publik</Text>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Laporan Publik</Text>
<Text fw="bold" fz="sm" mb={6}>
Penanganan Laporan Publik
</Text>
<CreateEditor
value={stateLaporan.create.form.penanganan}
onChange={(e) => stateLaporan.create.form.penanganan = e}
onChange={(e) => (stateLaporan.create.form.penanganan = e)}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>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>

View File

@@ -1,9 +1,26 @@
'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,
Paper,
Pagination,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import laporanPublikState from '../../_state/keamanan/laporan-publik';
@@ -16,74 +33,134 @@ function LaporanPublik() {
<Box>
<HeaderSearch
title='Laporan Publik'
placeholder='pencarian'
placeholder='Cari laporan publik...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListLaporanPublik search={search}/>
<ListLaporanPublik search={search} />
</Box>
);
}
function ListLaporanPublik({ search }: { search: string }) {
const stateLaporan = useProxy(laporanPublikState)
const stateLaporan = useProxy(laporanPublikState);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = stateLaporan.findMany;
useShallowEffect(() => {
stateLaporan.findMany.load()
}, [])
load(page, 10, search);
}, [page, search]);
const filteredData = (stateLaporan.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.judul.toLowerCase().includes(keyword) ||
item.status.toLowerCase().includes(keyword) ||
item.kronologi?.toLowerCase().includes(keyword)
);
});
const filteredData = data || [];
if (!stateLaporan.findMany.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 Laporan Publik'
href='/admin/keamanan/laporan-publik/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Judul Laporan Publik</TableTh>
<TableTh>Tanggal Laporan Publik</TableTh>
<TableTh>Status</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Laporan Publik</Title>
<Tooltip label="Tambah Laporan Publik" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/keamanan/laporan-publik/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Judul Laporan Publik</TableTh>
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
<TableTh style={{ width: '15%' }}>Status</TableTh>
<TableTh style={{ width: '25%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.judul}</TableTd>
<TableTd>{new Date(item.tanggalWaktu).toLocaleDateString('id-ID')}</TableTd>
<TableTd>{item.status}</TableTd>
<TableTd>{item.kronologi}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/laporan-publik/${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} truncate="end" lineClamp={1}>
{item.judul}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">
{new Date(item.tanggalWaktu).toLocaleDateString('id-ID')}
</Text>
</TableTd>
<TableTd>
<Text fz="sm">{item.status}</Text>
</TableTd>
<TableTd>
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
{item.kronologi}
</Text>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/keamanan/laporan-publik/${item.id}`)
}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data laporan publik 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>
);
}

View File

@@ -4,22 +4,27 @@
import {
Box,
Button,
Center,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title
Title,
Tooltip,
} from "@mantine/core";
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
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";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
@@ -34,24 +39,24 @@ function EditTipsKeamanan() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
judul: keamananState.update.form.judul || '',
deskripsi: keamananState.update.form.deskripsi || '',
imageId: keamananState.update.form.imageId || ''
judul: keamananState.update.form.judul || "",
deskripsi: keamananState.update.form.deskripsi || "",
imageId: keamananState.update.form.imageId || "",
});
// Load berita by id saat pertama kali
// Load data saat pertama kali
useEffect(() => {
const loadBerita = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await keamananState.update.load(id); // akses langsung, bukan dari proxy
const data = await keamananState.update.load(id);
if (data) {
setFormData({
judul: data.judul || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
judul: data.judul || "",
deskripsi: data.deskripsi || "",
imageId: data.imageId || "",
});
if (data?.image?.link) {
@@ -59,35 +64,30 @@ function EditTipsKeamanan() {
}
}
} catch (error) {
console.error("Error loading keamananLingkungan:", error);
toast.error("Gagal memuat data keamananLingkungan");
console.error("Error loading tips keamanan:", error);
toast.error("Gagal memuat data tips keamanan");
}
};
loadBerita();
}, [params?.id]); // ✅ hapus beritaState dari dependency
loadData();
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state with form data
keamananState.update.form = {
...keamananState.update.form,
judul: formData.judul,
deskripsi: formData.deskripsi,
imageId: formData.imageId // Keep existing imageId if not changed
...formData,
};
// Jika ada file baru, upload
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");
}
if (!uploaded?.id) return toast.error("Gagal upload gambar");
// Update imageId in global state
keamananState.update.form.imageId = uploaded.id;
}
@@ -95,71 +95,119 @@ function EditTipsKeamanan() {
toast.success("Tips Keamanan berhasil diperbarui!");
router.push("/admin/keamanan/tips-keamanan");
} catch (error) {
console.error("Error updating keamananLingkungan:", error);
toast.error("Terjadi kesalahan saat memperbarui keamananLingkungan");
console.error("Error updating tips keamanan:", error);
toast.error("Terjadi kesalahan saat memperbarui tips keamanan");
}
};
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 Tips Keamanan</Title>
<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>
<Box px={{ base: "sm", md: "lg" }} py="md">
{/* Header */}
<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 Tips Keamanan
</Title>
</Group>
<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>
{/* Form Card */}
<Paper
w={{ base: "100%", md: "60%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
{/* Dropzone Upload */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Tips Keamanan
</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 ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
{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
mt="sm"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: 200,
border: "1px dashed #ccc",
borderRadius: 8,
}}
>
<IconImageInPicture size={48} color="#868e96" />
</Box>
)}
</Box>
{/* Input Judul */}
<TextInput
label="Nama Tips Keamanan"
placeholder="Masukkan nama tips keamanan"
value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama Tips Keamanan</Text>}
placeholder="masukkan nama tips keamanan"
required
/>
{/* Input Deskripsi */}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -169,7 +217,21 @@ function EditTipsKeamanan() {
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
{/* Button Simpan */}
<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>

View File

@@ -1,9 +1,8 @@
'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 { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -11,101 +10,134 @@ import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import tipsKeamananState from '../../../_state/keamanan/tips-keamanan';
function DetailTipsKeamanan() {
const stateKeamanan = useProxy(tipsKeamananState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
const stateKeamanan = useProxy(tipsKeamananState);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
stateKeamanan.findUnique.load(params?.id as string)
}, [])
stateKeamanan.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
stateKeamanan.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/keamanan/tips-keamanan")
stateKeamanan.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/keamanan/tips-keamanan");
}
}
};
if (!stateKeamanan.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
);
}
const data = stateKeamanan.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 Tips Keamanan</Text>
{stateKeamanan.findUnique.data ? (
<Paper key={stateKeamanan.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Tips Keamanan</Text>
<Text fz={"lg"}>{stateKeamanan.findUnique.data?.judul}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateKeamanan.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateKeamanan.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 Tips Keamanan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama Tips Keamanan</Text>
<Text fz="md" c="dimmed">{data.judul || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.judul || 'Gambar Tips Keamanan'}
w={150}
h={150}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Group gap="sm">
<Tooltip label="Hapus Tips Keamanan" withArrow position="top">
<Button
color="red"
onClick={() => {
if (stateKeamanan.findUnique.data) {
setSelectedId(stateKeamanan.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={stateKeamanan.delete.loading || !stateKeamanan.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
disabled={stateKeamanan.delete.loading}
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Tips Keamanan" withArrow position="top">
<Button
onClick={() => {
if (stateKeamanan.findUnique.data) {
router.push(`/admin/keamanan/tips-keamanan/${stateKeamanan.findUnique.data.id}/edit`);
}
}}
disabled={!stateKeamanan.findUnique.data}
color={"green"}
color="green"
onClick={() => router.push(`/admin/keamanan/tips-keamanan/${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 tips keamanan ini?'
text="Apakah anda yakin ingin menghapus tips keamanan ini?"
/>
</Box>
);
}
export default DetailTipsKeamanan;
export default DetailTipsKeamanan;

View File

@@ -1,7 +1,18 @@
'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,37 +22,36 @@ import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import tipsKeamananState from '../../../_state/keamanan/tips-keamanan';
function CreateKeamananLingkungan() {
const stateKeamanan = useProxy(tipsKeamananState)
const stateKeamanan = useProxy(tipsKeamananState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter();
const resetForm = () => {
stateKeamanan.create.form = {
judul: "",
deskripsi: "",
imageId: "",
}
judul: '',
deskripsi: '',
imageId: '',
};
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');
}
stateKeamanan.create.form.imageId = uploaded.id;
@@ -49,86 +59,94 @@ function CreateKeamananLingkungan() {
await stateKeamanan.create.create();
resetForm();
router.push("/admin/keamanan/tips-keamanan")
}
router.push('/admin/keamanan/tips-keamanan');
};
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">
{/* Header Back + Title */}
<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 Tips Keamanan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Tips Keamanan</Title>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Upload Image */}
<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 Tips Keamanan
</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>
{/* Input Judul */}
<TextInput
value={stateKeamanan.create.form.judul}
onChange={(val) => {
stateKeamanan.create.form.judul = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Tips Keamanan</Text>}
placeholder='Masukkan nama Tips Keamanan'
label="Judul Tips Keamanan"
placeholder="Masukkan judul tips keamanan"
value={stateKeamanan.create.form.judul || ''}
onChange={(e) => (stateKeamanan.create.form.judul = e.target.value)}
required
/>
{/* Editor Deskripsi */}
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Tips Keamanan
</Text>
<CreateEditor
value={stateKeamanan.create.form.deskripsi}
onChange={(val) => {
@@ -136,8 +154,21 @@ function CreateKeamananLingkungan() {
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
{/* 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>

View File

@@ -1,14 +1,31 @@
'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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import tipsKeamananState from '../../_state/keamanan/tips-keamanan';
import { useProxy } from 'valtio/utils';
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 { useState } from 'react';
import HeaderSearch from '../../_com/header';
import tipsKeamananState from '../../_state/keamanan/tips-keamanan';
function TipsKeamanan() {
const [search, setSearch] = useState("");
@@ -16,12 +33,12 @@ function TipsKeamanan() {
<Box>
<HeaderSearch
title='Tips Keamanan'
placeholder='pencarian'
placeholder='Cari tips keamanan...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListTipsKeamanan search={search}/>
<ListTipsKeamanan search={search} />
</Box>
);
}
@@ -47,54 +64,85 @@ function ListTipsKeamanan({ 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'}>
<JudulList
title='List Tips Keamanan'
href='/admin/keamanan/tips-keamanan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Tips Keamanan</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Tips Keamanan</Title>
<Tooltip label="Tambah Tips Keamanan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/keamanan/tips-keamanan/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Judul</TableTh>
<TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={150}>
<Text fz={"md"} truncate={"end"} lineClamp={1}>{item.judul}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={250}>
<Text fz={"md"} truncate={"end"} lineClamp={1} dangerouslySetInnerHTML={{__html: item.deskripsi}} />
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/tips-keamanan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Text fz="sm" c="dimmed" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/keamanan/tips-keamanan/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data tips keamanan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
onChange={(newPage) => {
load(newPage, 10)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
total={totalPages}
my="md"
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -1,139 +1,146 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconActivity, IconBuildingHospital, IconCalendarEvent, IconGauge, IconNotes } from '@tabler/icons-react';
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 router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "Presentase Kelahiran & Kematian",
value: "presentasekelahiran&kematian",
href: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian",
icon: <IconActivity size={18} stroke={1.8} />,
tooltip: "Lihat data kelahiran dan kematian"
},
{
label: "Grafik Hasil Kepuasan Masyarakat",
value: "grafikhasilkepuasan",
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
icon: <IconGauge size={18} stroke={1.8} />,
tooltip: "Grafik kepuasan masyarakat terhadap pelayanan"
},
{
label: "Fasilitas Kesehatan",
value: "fasilitaskesehatan",
href: "/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan",
icon: <IconBuildingHospital size={18} stroke={1.8} />,
tooltip: "Data fasilitas kesehatan desa"
},
{
label: "Jadwal Kegiatan",
value: "jadwalkegiatan",
href: "/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan",
icon: <IconCalendarEvent size={18} stroke={1.8} />,
tooltip: "Atur jadwal kegiatan kesehatan"
},
{
label: "Artikel Kesehatan",
value: "artikelkesehatan",
href: "/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan",
icon: <IconNotes size={18} stroke={1.8} />,
tooltip: "Artikel & informasi seputar kesehatan"
},
];
const tabs = [
{
label: "Presentase Kelahiran & Kematian",
value: "presentasekelahiran&kematian",
href: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian",
icon: <IconActivity size={18} stroke={1.8} />,
tooltip: "Lihat data kelahiran dan kematian"
},
{
label: "Grafik Hasil Kepuasan Masyarakat",
value: "grafikhasilkepuasan",
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
icon: <IconGauge size={18} stroke={1.8} />,
tooltip: "Grafik kepuasan masyarakat terhadap pelayanan"
},
{
label: "Fasilitas Kesehatan",
value: "fasilitaskesehatan",
href: "/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan",
icon: <IconBuildingHospital size={18} stroke={1.8} />,
tooltip: "Data fasilitas kesehatan desa"
},
{
label: "Jadwal Kegiatan",
value: "jadwalkegiatan",
href: "/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan",
icon: <IconCalendarEvent size={18} stroke={1.8} />,
tooltip: "Atur jadwal kegiatan kesehatan"
},
{
label: "Artikel Kesehatan",
value: "artikelkesehatan",
href: "/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan",
icon: <IconNotes size={18} stroke={1.8} />,
tooltip: "Artikel & informasi seputar kesehatan"
},
];
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.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);
if (tab) {
router.push(tab.href);
}
setActiveTab(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 gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Data Kesehatan Warga
</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>
return (
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Data Kesehatan Warga
</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{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>
</ScrollArea>
{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>
</Stack>
);
{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>
</Stack>
);
}

View File

@@ -1,10 +1,20 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
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';
"use client";
import { usePathname, useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import {
Stack,
Tabs,
TabsList,
TabsPanel,
TabsTab,
Title,
Tooltip,
ScrollArea,
} from "@mantine/core";
import { IconList, IconCategory } from "@tabler/icons-react";
import colors from "@/con/colors";
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter();
@@ -27,19 +37,21 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
},
];
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.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);
}
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);
}
@@ -51,37 +63,51 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
Desa Anti Korupsi
</Title>
<Tabs
color={colors['blue-button']}
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",
}}
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: "pop", duration: 200 }}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ mencegah tab mengecil
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
@@ -102,4 +128,4 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
);
}
export default LayoutTabs;
export default LayoutTabs;

View File

@@ -80,7 +80,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>

View File

@@ -75,64 +75,65 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table
striped
highlightOnHover
<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 style={{ width: '50%' }}>
<Text fw={500} lineClamp={1}>
{item.name || '-'}
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 style={{ width: '50%' }}>
<Text fw={500} lineClamp={1}>
{item.name || '-'}
</Text>
</TableTd>
<TableTd style={{ width: '30%' }}>
<Text fz="sm" c="dimmed">
{item.kategori?.name || '-'}
</Text>
</TableTd>
<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>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Text ta="center" c="dimmed">
Tidak ditemukan data dengan kata kunci pencarian
</Text>
</TableTd>
<TableTd style={{ width: '30%' }}>
<Text fz="sm" c="dimmed">
{item.kategori?.name || '-'}
</Text>
</TableTd>
<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>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Text ta="center" c="dimmed">
Tidak ditemukan data dengan kata kunci pencarian
</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { ScrollArea, 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';
@@ -46,36 +46,44 @@ function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) {
Indeks Kepuasan Masyarakat
</Title>
<Tabs
radius="xl"
color={colors['blue-button']}
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<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>
{/* ✅ Scroll horizontal wrapper */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{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>
</ScrollArea>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value} mt="md">
<></>

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