QC Admin - User Menu Ekonomi : Jumlah Pengangguran
This commit is contained in:
@@ -1,51 +1,99 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"month": "Jan",
|
"month": "Jan",
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"totalUnemployment": 160,
|
"totalUnemployment": 160,
|
||||||
"educatedUnemployment": 95,
|
"educatedUnemployment": 95,
|
||||||
"uneducatedUnemployment": 65,
|
"uneducatedUnemployment": 65,
|
||||||
"percentageChange": null
|
"percentageChange": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"month": "Feb",
|
"month": "Feb",
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"totalUnemployment": 155,
|
"totalUnemployment": 158,
|
||||||
"educatedUnemployment": 90,
|
"educatedUnemployment": 93,
|
||||||
"uneducatedUnemployment": 65,
|
"uneducatedUnemployment": 65,
|
||||||
"percentageChange": -3.1
|
"percentageChange": -1.25
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"month": "Mar",
|
"month": "Mar",
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"totalUnemployment": 150,
|
"totalUnemployment": 155,
|
||||||
"educatedUnemployment": 88,
|
"educatedUnemployment": 91,
|
||||||
"uneducatedUnemployment": 62,
|
"uneducatedUnemployment": 64,
|
||||||
"percentageChange": -3.2
|
"percentageChange": -1.90
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"month": "Apr",
|
"month": "Apr",
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"totalUnemployment": 148,
|
"totalUnemployment": 152,
|
||||||
"educatedUnemployment": 85,
|
"educatedUnemployment": 89,
|
||||||
"uneducatedUnemployment": 63,
|
"uneducatedUnemployment": 63,
|
||||||
"percentageChange": -1.3
|
"percentageChange": -1.94
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"month": "Mei",
|
"month": "Mei",
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"totalUnemployment": 145,
|
"totalUnemployment": 150,
|
||||||
"educatedUnemployment": 82,
|
"educatedUnemployment": 88,
|
||||||
"uneducatedUnemployment": 63,
|
"uneducatedUnemployment": 62,
|
||||||
"percentageChange": -2.0
|
"percentageChange": -1.32
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"month": "Jun",
|
"month": "Jun",
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"totalUnemployment": 140,
|
"totalUnemployment": 148,
|
||||||
"educatedUnemployment": 80,
|
"educatedUnemployment": 87,
|
||||||
"uneducatedUnemployment": 60,
|
"uneducatedUnemployment": 61,
|
||||||
"percentageChange": -3.4
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ model KategoriDesaAntiKorupsi {
|
|||||||
//========================================= SDGS Desa ========================================= //
|
//========================================= SDGS Desa ========================================= //
|
||||||
model SdgsDesa {
|
model SdgsDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String
|
||||||
jumlah String
|
jumlah String
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
@@ -183,7 +183,7 @@ model SdgsDesa {
|
|||||||
//========================================= APBDes ========================================= //
|
//========================================= APBDes ========================================= //
|
||||||
model APBDes {
|
model APBDes {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String
|
||||||
jumlah String
|
jumlah String
|
||||||
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
|
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
@@ -198,7 +198,7 @@ model APBDes {
|
|||||||
//========================================= PRESTASI DESA ========================================= //
|
//========================================= PRESTASI DESA ========================================= //
|
||||||
model PrestasiDesa {
|
model PrestasiDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
|
kategori KategoriPrestasiDesa @relation(fields: [kategoriId], references: [id])
|
||||||
kategoriId String
|
kategoriId String
|
||||||
@@ -1416,6 +1416,9 @@ model PosisiOrganisasi {
|
|||||||
pegawai Pegawai[]
|
pegawai Pegawai[]
|
||||||
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
strukturOrganisasi StrukturOrganisasi[] // Relasi balik
|
||||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@map("posisi_organisasi")
|
@@map("posisi_organisasi")
|
||||||
}
|
}
|
||||||
@@ -1566,7 +1569,7 @@ model DataDemografiPekerjaan {
|
|||||||
model DetailDataPengangguran {
|
model DetailDataPengangguran {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
month String @db.VarChar(20)
|
month String @db.VarChar(20)
|
||||||
year DateTime
|
year Int
|
||||||
totalUnemployment Int
|
totalUnemployment Int
|
||||||
educatedUnemployment Int
|
educatedUnemployment Int
|
||||||
uneducatedUnemployment Int
|
uneducatedUnemployment Int
|
||||||
|
|||||||
@@ -376,8 +376,7 @@ import fileStorage from "./data/file-storage.json";
|
|||||||
for (const l of apbdes) {
|
for (const l of apbdes) {
|
||||||
await prisma.aPBDes.upsert({
|
await prisma.aPBDes.upsert({
|
||||||
where: {
|
where: {
|
||||||
name: l.name,
|
id: l.id,
|
||||||
jumlah: l.jumlah,
|
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
name: l.name,
|
name: l.name,
|
||||||
@@ -895,12 +894,9 @@ import fileStorage from "./data/file-storage.json";
|
|||||||
console.log("hubungan organisasi success ...");
|
console.log("hubungan organisasi success ...");
|
||||||
|
|
||||||
for (const d of detailDataPengangguran) {
|
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({
|
await prisma.detailDataPengangguran.upsert({
|
||||||
where: {
|
where: {
|
||||||
month_year: { month: d.month, year: yearAsDate },
|
month_year: { month: d.month, year: d.year },
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
totalUnemployment: d.totalUnemployment,
|
totalUnemployment: d.totalUnemployment,
|
||||||
@@ -910,7 +906,7 @@ import fileStorage from "./data/file-storage.json";
|
|||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
month: d.month,
|
month: d.month,
|
||||||
year: yearAsDate,
|
year: d.year,
|
||||||
totalUnemployment: d.totalUnemployment,
|
totalUnemployment: d.totalUnemployment,
|
||||||
educatedUnemployment: d.educatedUnemployment,
|
educatedUnemployment: d.educatedUnemployment,
|
||||||
uneducatedUnemployment: d.uneducatedUnemployment,
|
uneducatedUnemployment: d.uneducatedUnemployment,
|
||||||
|
|||||||
BIN
public/perbekel.png
Normal file
BIN
public/perbekel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
@@ -7,9 +7,13 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const templateApbDesa = z.object({
|
const templateApbDesa = z.object({
|
||||||
tahun: z.number().min(4, "Tahun minimal 4 karakter"),
|
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"),
|
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 = {
|
const ApbDesaDefaultForm = {
|
||||||
@@ -54,32 +58,46 @@ const ApbDesa = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.ApbDesaGetPayload<{
|
| Prisma.ApbDesaGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
pendapatan: true;
|
pendapatan: true;
|
||||||
belanja: true;
|
belanja: true;
|
||||||
pembiayaan: true;
|
pembiayaan: true;
|
||||||
};
|
};
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
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 {
|
try {
|
||||||
this.loading = true;
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
|
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get();
|
].get({ query });
|
||||||
if (res.status === 200) {
|
|
||||||
this.data = res.data?.data ?? [];
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
ApbDesa.findMany.data = res.data.data ?? [];
|
||||||
|
ApbDesa.findMany.totalPages =
|
||||||
|
res.data.totalPages ?? 1;
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.data?.message || "Gagal mengambil APB Desa");
|
ApbDesa.findMany.data = [];
|
||||||
|
ApbDesa.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Find many error:", error);
|
console.error("Gagal fetch APB Desa paginated:", err);
|
||||||
toast.error("Gagal mengambil APB Desa");
|
ApbDesa.findMany.data = [];
|
||||||
|
ApbDesa.findMany.totalPages = 1;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
ApbDesa.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -106,13 +124,13 @@ const ApbDesa = proxy({
|
|||||||
throw new Error("Gagal mengambil APB Desa");
|
throw new Error("Gagal mengambil APB Desa");
|
||||||
}
|
}
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.message || "Gagal memuat APB Desa");
|
throw new Error(result.message || "Gagal memuat APB Desa");
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.form = {
|
this.form = {
|
||||||
tahun: data.tahun || 0,
|
tahun: data.tahun || 0,
|
||||||
@@ -120,7 +138,7 @@ const ApbDesa = proxy({
|
|||||||
belanjaIds: data.belanja?.map((b: any) => b.id) || [],
|
belanjaIds: data.belanja?.map((b: any) => b.id) || [],
|
||||||
pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
|
pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading APB Desa:", error);
|
console.error("Error loading APB Desa:", error);
|
||||||
@@ -189,7 +207,7 @@ const ApbDesa = proxy({
|
|||||||
data: null as Prisma.ApbDesaGetPayload<{
|
data: null as Prisma.ApbDesaGetPayload<{
|
||||||
include: { pendapatan: true; belanja: true; pembiayaan: true };
|
include: { pendapatan: true; belanja: true; pembiayaan: true };
|
||||||
}> | null,
|
}> | null,
|
||||||
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -199,11 +217,11 @@ const ApbDesa = proxy({
|
|||||||
throw new Error("Gagal mengambil detail APB Desa");
|
throw new Error("Gagal mengambil detail APB Desa");
|
||||||
}
|
}
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.message || "Gagal mengambil data");
|
throw new Error(result.message || "Gagal mengambil data");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.data = result.data; // ✅ fix utama di sini
|
this.data = result.data; // ✅ fix utama di sini
|
||||||
return result.data;
|
return result.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -264,34 +282,32 @@ const pendapatan = proxy({
|
|||||||
data: null as any[] | null,
|
data: null as any[] | null,
|
||||||
page: 1,
|
page: 1,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
total: 0,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
load: async (page = 1, limit = 10) => {
|
search: "",
|
||||||
// Change to arrow function
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
pendapatan.findMany.loading = true; // Use the full path to access the property
|
pendapatan.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
pendapatan.findMany.page = page;
|
pendapatan.findMany.page = page;
|
||||||
|
pendapatan.findMany.search = search;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res =
|
const query: any = { page, limit };
|
||||||
await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[
|
if (search) query.search = search;
|
||||||
"find-many"
|
|
||||||
].get({
|
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[
|
||||||
query: { page, limit },
|
"find-many"
|
||||||
});
|
].get({ query });
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
pendapatan.findMany.data = res.data.data || [];
|
pendapatan.findMany.data = res.data.data ?? [];
|
||||||
pendapatan.findMany.total = res.data.total || 0;
|
pendapatan.findMany.totalPages =
|
||||||
pendapatan.findMany.totalPages = res.data.totalPages || 1;
|
res.data.totalPages ?? 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load pendapatan:", res.data?.message);
|
|
||||||
pendapatan.findMany.data = [];
|
pendapatan.findMany.data = [];
|
||||||
pendapatan.findMany.total = 0;
|
|
||||||
pendapatan.findMany.totalPages = 1;
|
pendapatan.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Error loading pendapatan:", error);
|
console.error("Gagal fetch pendapatan asli desa paginated:", err);
|
||||||
pendapatan.findMany.data = [];
|
pendapatan.findMany.data = [];
|
||||||
pendapatan.findMany.total = 0;
|
|
||||||
pendapatan.findMany.totalPages = 1;
|
pendapatan.findMany.totalPages = 1;
|
||||||
} finally {
|
} finally {
|
||||||
pendapatan.findMany.loading = false;
|
pendapatan.findMany.loading = false;
|
||||||
@@ -308,12 +324,15 @@ const pendapatan = proxy({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`, {
|
const response = await fetch(
|
||||||
method: "GET",
|
`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "GET",
|
||||||
},
|
headers: {
|
||||||
});
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -349,16 +368,19 @@ const pendapatan = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
pendapatan.update.loading = true;
|
pendapatan.update.loading = true;
|
||||||
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${this.id}`, {
|
const response = await fetch(
|
||||||
method: "PUT",
|
`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${this.id}`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "PUT",
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify({
|
"Content-Type": "application/json",
|
||||||
name: this.form.name,
|
},
|
||||||
value: this.form.value,
|
body: JSON.stringify({
|
||||||
}),
|
name: this.form.name,
|
||||||
});
|
value: this.form.value,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -495,23 +517,37 @@ const belanja = proxy({
|
|||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: number;
|
||||||
}>,
|
}>,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
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 {
|
try {
|
||||||
this.loading = true;
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[
|
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get();
|
].get({ query });
|
||||||
if (res.status === 200) {
|
|
||||||
this.data = res.data?.data ?? [];
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
belanja.findMany.data = res.data.data ?? [];
|
||||||
|
belanja.findMany.totalPages =
|
||||||
|
res.data.totalPages ?? 1;
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.data?.message || "Gagal mengambil Belanja");
|
belanja.findMany.data = [];
|
||||||
|
belanja.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Find many error:", error);
|
console.error("Gagal fetch Belanja paginated:", err);
|
||||||
toast.error("Gagal mengambil Belanja");
|
belanja.findMany.data = [];
|
||||||
|
belanja.findMany.totalPages = 1;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
belanja.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -525,12 +561,15 @@ const belanja = proxy({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${id}`, {
|
const response = await fetch(
|
||||||
method: "GET",
|
`/api/ekonomi/pendapatanaslidesa/belanja/${id}`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "GET",
|
||||||
},
|
headers: {
|
||||||
});
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -566,16 +605,19 @@ const belanja = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
belanja.update.loading = true;
|
belanja.update.loading = true;
|
||||||
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`, {
|
const response = await fetch(
|
||||||
method: "PUT",
|
`/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "PUT",
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify({
|
"Content-Type": "application/json",
|
||||||
name: this.form.name,
|
},
|
||||||
value: this.form.value,
|
body: JSON.stringify({
|
||||||
}),
|
name: this.form.name,
|
||||||
});
|
value: this.form.value,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -710,23 +752,37 @@ const pembiayaan = proxy({
|
|||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: number;
|
||||||
}>,
|
}>,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
loading: false,
|
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 {
|
try {
|
||||||
this.loading = true;
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[
|
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[
|
||||||
"find-many"
|
"find-many"
|
||||||
].get();
|
].get({ query });
|
||||||
if (res.status === 200) {
|
|
||||||
this.data = res.data?.data ?? [];
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
pembiayaan.findMany.data = res.data.data ?? [];
|
||||||
|
pembiayaan.findMany.totalPages =
|
||||||
|
res.data.totalPages ?? 1;
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.data?.message || "Gagal mengambil Pembiayaan");
|
pembiayaan.findMany.data = [];
|
||||||
|
pembiayaan.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Find many error:", error);
|
console.error("Gagal fetch Pembiayaan paginated:", err);
|
||||||
toast.error("Gagal mengambil Pembiayaan");
|
pembiayaan.findMany.data = [];
|
||||||
|
pembiayaan.findMany.totalPages = 1;
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
pembiayaan.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -740,12 +796,15 @@ const pembiayaan = proxy({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`, {
|
const response = await fetch(
|
||||||
method: "GET",
|
`/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "GET",
|
||||||
},
|
headers: {
|
||||||
});
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -781,16 +840,19 @@ const pembiayaan = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
pembiayaan.update.loading = true;
|
pembiayaan.update.loading = true;
|
||||||
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`, {
|
const response = await fetch(
|
||||||
method: "PUT",
|
`/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "PUT",
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify({
|
"Content-Type": "application/json",
|
||||||
name: this.form.name,
|
},
|
||||||
value: this.form.value,
|
body: JSON.stringify({
|
||||||
}),
|
name: this.form.name,
|
||||||
});
|
value: this.form.value,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ const templateJumlahPengngguran = z.object({
|
|||||||
uneducatedUnemployment: z
|
uneducatedUnemployment: z
|
||||||
.number()
|
.number()
|
||||||
.min(1, "Pengangguran tidak pendidikan harus diisi"),
|
.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 = {
|
type JumlahPengangguran = {
|
||||||
@@ -29,7 +30,7 @@ type JumlahPengangguran = {
|
|||||||
|
|
||||||
const jumlahPengangguranForm: JumlahPengangguran = {
|
const jumlahPengangguranForm: JumlahPengangguran = {
|
||||||
month: "",
|
month: "",
|
||||||
year: 0,
|
year: new Date().getFullYear(), // Default to current year
|
||||||
totalUnemployment: 0,
|
totalUnemployment: 0,
|
||||||
educatedUnemployment: 0,
|
educatedUnemployment: 0,
|
||||||
uneducatedUnemployment: 0,
|
uneducatedUnemployment: 0,
|
||||||
@@ -60,13 +61,21 @@ const jumlahPengangguran = proxy({
|
|||||||
form: jumlahPengangguranForm,
|
form: jumlahPengangguranForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
async create() {
|
||||||
const cek = templateJumlahPengngguran.safeParse(
|
// Ensure all number fields are actual numbers
|
||||||
jumlahPengangguran.create.form
|
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) {
|
if (!cek.success) {
|
||||||
const err = `[${cek.error.issues
|
const err = `[${cek.error.issues
|
||||||
.map((v) => `${v.path.join(".")}`)
|
.map((v) => `${v.path.join(".")} (${v.message})`)
|
||||||
.join("\n")}] required`;
|
.join("\n")}]`;
|
||||||
toast.error(err);
|
toast.error(err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -78,7 +87,7 @@ const jumlahPengangguran = proxy({
|
|||||||
].post(jumlahPengangguran.create.form);
|
].post(jumlahPengangguran.create.form);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const id = res.data?.data?.id;
|
const id = res.data?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
toast.success("Success create");
|
toast.success("Success create");
|
||||||
jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
|
jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
|
||||||
@@ -103,16 +112,40 @@ const jumlahPengangguran = proxy({
|
|||||||
omit: { isActive: true };
|
omit: { isActive: true };
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res =
|
totalPages: 1,
|
||||||
await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
|
loading: false,
|
||||||
"find-many"
|
search: "",
|
||||||
].get();
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
if (res.status === 200) {
|
jumlahPengangguran.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
jumlahPengangguran.findMany.data = res.data?.data ?? [];
|
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: {
|
findUnique: {
|
||||||
data: null as Prisma.DetailDataPengangguranGetPayload<{
|
data: null as Prisma.DetailDataPengangguranGetPayload<{
|
||||||
|
|||||||
@@ -161,18 +161,34 @@ const posisiOrganisasi = proxy({
|
|||||||
deskripsi: string | null;
|
deskripsi: string | null;
|
||||||
hierarki: number;
|
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 {
|
try {
|
||||||
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
|
const query: any = { page, limit };
|
||||||
"posisi-organisasi"
|
if (search) query.search = search;
|
||||||
]["find-many"].get();
|
|
||||||
if (res.status === 200) {
|
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get({ query });
|
||||||
// The API now returns the id field, so we can use it directly
|
|
||||||
this.data = res.data?.data ?? [];
|
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) {
|
} catch (err) {
|
||||||
console.error("Find many error:", error);
|
console.error("Gagal fetch posisi organisasi paginated:", err);
|
||||||
this.data = [];
|
posisiOrganisasi.findMany.data = [];
|
||||||
|
posisiOrganisasi.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
posisiOrganisasi.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -97,13 +98,37 @@ const laporanPublikState = proxy({
|
|||||||
include: { penanganan: true };
|
include: { penanganan: true };
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.keamanan.laporanpublik["find-many"].get();
|
totalPages: 1,
|
||||||
if (res.status === 200) {
|
loading: false,
|
||||||
laporanPublikState.findMany.data = res.data?.data ?? [];
|
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: {
|
findUnique: {
|
||||||
data: null as Prisma.LaporanPublikGetPayload<{
|
data: null as Prisma.LaporanPublikGetPayload<{
|
||||||
include: { penanganan: true };
|
include: { penanganan: true };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
|
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
|
||||||
@@ -62,43 +62,50 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Layanan</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant='pills'
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
}}
|
borderRadius: "1rem",
|
||||||
>
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
{tabs.map((tab, i) => (
|
display: "flex",
|
||||||
<Tooltip
|
flexWrap: "nowrap",
|
||||||
key={i}
|
gap: "0.5rem",
|
||||||
label={tab.tooltip}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
position="bottom"
|
}}
|
||||||
withArrow
|
>
|
||||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
{tabs.map((tab, i) => (
|
||||||
>
|
<Tooltip
|
||||||
<TabsTab
|
key={i}
|
||||||
value={tab.value}
|
label={tab.tooltip}
|
||||||
leftSection={tab.icon}
|
position="bottom"
|
||||||
style={{
|
withArrow
|
||||||
fontWeight: 600,
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
fontSize: "0.9rem",
|
|
||||||
transition: "all 0.2s ease",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{tab.label}
|
<TabsTab
|
||||||
</TabsTab>
|
value={tab.value}
|
||||||
</Tooltip>
|
leftSection={tab.icon}
|
||||||
))}
|
style={{
|
||||||
</TabsList>
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { IconNews, IconCategory } from '@tabler/icons-react';
|
import { IconNews, IconCategory } from '@tabler/icons-react';
|
||||||
@@ -49,43 +49,50 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Berita Desa</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant="pills"
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
}}
|
borderRadius: "1rem",
|
||||||
>
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
{tabs.map((tab, i) => (
|
display: "flex",
|
||||||
<Tooltip
|
flexWrap: "nowrap",
|
||||||
key={i}
|
gap: "0.5rem",
|
||||||
label={tab.tooltip}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
position="bottom"
|
}}
|
||||||
withArrow
|
>
|
||||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
{tabs.map((tab, i) => (
|
||||||
>
|
<Tooltip
|
||||||
<TabsTab
|
key={i}
|
||||||
value={tab.value}
|
label={tab.tooltip}
|
||||||
leftSection={tab.icon}
|
position="bottom"
|
||||||
style={{
|
withArrow
|
||||||
fontWeight: 600,
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
fontSize: "0.9rem",
|
|
||||||
transition: "all 0.2s ease",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{tab.label}
|
<TabsTab
|
||||||
</TabsTab>
|
value={tab.value}
|
||||||
</Tooltip>
|
leftSection={tab.icon}
|
||||||
))}
|
style={{
|
||||||
</TabsList>
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
|
|||||||
@@ -96,9 +96,11 @@ function ListBerita({ search }: { search: string }) {
|
|||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '30%' }}>
|
<TableTd style={{ width: '30%' }}>
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>
|
<Box w={200}>
|
||||||
{item.judul}
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
</Text>
|
{item.judul}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%' }}>
|
<TableTd style={{ width: '20%' }}>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz="sm" c="dimmed">
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export default function ListImage() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
stateFileStorage
|
stateFileStorage
|
||||||
.del({ name: v.name })
|
.del({ id: v.id })
|
||||||
.finally(() => toast("Foto berhasil dihapus"));
|
.finally(() => toast("Foto berhasil dihapus"));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -143,14 +143,15 @@ export default function ListImage() {
|
|||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
<Pagination
|
<Pagination
|
||||||
total={total}
|
total={total}
|
||||||
|
value={stateFileStorage.page} // Changed from page to value
|
||||||
size="md"
|
size="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
withEdges
|
withEdges
|
||||||
onChange={(page) => {
|
onChange={(page) => {
|
||||||
stateFileStorage.page = page;
|
stateFileStorage.load({ page });
|
||||||
stateFileStorage.load();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { IconPhoto, IconVideo } from '@tabler/icons-react';
|
import { IconPhoto, IconVideo } from '@tabler/icons-react';
|
||||||
@@ -48,43 +48,50 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Gallery</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant='pills'
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
}}
|
borderRadius: "1rem",
|
||||||
>
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
{tabs.map((tab, i) => (
|
display: "flex",
|
||||||
<Tooltip
|
flexWrap: "nowrap",
|
||||||
key={i}
|
gap: "0.5rem",
|
||||||
label={tab.tooltip}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
position="bottom"
|
}}
|
||||||
withArrow
|
>
|
||||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
{tabs.map((tab, i) => (
|
||||||
>
|
<Tooltip
|
||||||
<TabsTab
|
key={i}
|
||||||
value={tab.value}
|
label={tab.tooltip}
|
||||||
leftSection={tab.icon}
|
position="bottom"
|
||||||
style={{
|
withArrow
|
||||||
fontWeight: 600,
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
fontSize: "0.9rem",
|
|
||||||
transition: "all 0.2s ease",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{tab.label}
|
<TabsTab
|
||||||
</TabsTab>
|
value={tab.value}
|
||||||
</Tooltip>
|
leftSection={tab.icon}
|
||||||
))}
|
style={{
|
||||||
</TabsList>
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { IconListDetails, IconCategory } from '@tabler/icons-react';
|
import { IconListDetails, IconCategory } from '@tabler/icons-react';
|
||||||
@@ -48,37 +48,44 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Pengumuman</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Pengumuman</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant='pills'
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
}}
|
borderRadius: "1rem",
|
||||||
>
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
{tabs.map((tab, i) => (
|
display: "flex",
|
||||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
flexWrap: "nowrap",
|
||||||
<TabsTab
|
gap: "0.5rem",
|
||||||
value={tab.value}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
leftSection={tab.icon}
|
}}
|
||||||
style={{
|
>
|
||||||
fontWeight: 600,
|
{tabs.map((tab, i) => (
|
||||||
fontSize: "0.9rem",
|
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
transition: "all 0.2s ease",
|
<TabsTab
|
||||||
}}
|
value={tab.value}
|
||||||
>
|
leftSection={tab.icon}
|
||||||
{tab.label}
|
style={{
|
||||||
</TabsTab>
|
fontWeight: 600,
|
||||||
</Tooltip>
|
fontSize: "0.9rem",
|
||||||
))}
|
transition: "all 0.2s ease",
|
||||||
</TabsList>
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { IconCategory, IconListCheck } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@@ -48,37 +48,44 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Potensi</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Potensi</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant='pills'
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
}}
|
borderRadius: "1rem",
|
||||||
>
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
{tabs.map((tab, i) => (
|
display: "flex",
|
||||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
flexWrap: "nowrap",
|
||||||
<TabsTab
|
gap: "0.5rem",
|
||||||
value={tab.value}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
leftSection={tab.icon}
|
}}
|
||||||
style={{
|
>
|
||||||
fontWeight: 600,
|
{tabs.map((tab, i) => (
|
||||||
fontSize: "0.9rem",
|
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
transition: "all 0.2s ease",
|
<TabsTab
|
||||||
}}
|
value={tab.value}
|
||||||
>
|
leftSection={tab.icon}
|
||||||
{tab.label}
|
style={{
|
||||||
</TabsTab>
|
fontWeight: 600,
|
||||||
</Tooltip>
|
fontSize: "0.9rem",
|
||||||
))}
|
transition: "all 0.2s ease",
|
||||||
</TabsList>
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
|
import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
|
||||||
@@ -55,56 +55,63 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
|
<Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant='pills'
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tabs.map((tab, i) => (
|
|
||||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
|
||||||
<TabsTab
|
|
||||||
value={tab.value}
|
|
||||||
leftSection={tab.icon}
|
|
||||||
style={{
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
transition: "all 0.2s ease",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</TabsTab>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
|
||||||
<TabsPanel
|
|
||||||
key={i}
|
|
||||||
value={tab.value}
|
|
||||||
style={{
|
style={{
|
||||||
padding: "1.5rem",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
|
||||||
borderRadius: "1rem",
|
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 */}
|
{tabs.map((tab, i) => (
|
||||||
<>{children}</>
|
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
</TabsPanel>
|
<TabsTab
|
||||||
))}
|
value={tab.value}
|
||||||
</Tabs>
|
leftSection={tab.icon}
|
||||||
</Stack>
|
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;
|
||||||
|
|||||||
@@ -1,73 +1,150 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
IconFileAnalytics,
|
||||||
|
IconCoins,
|
||||||
|
IconShoppingCart,
|
||||||
|
IconWallet,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const pathname = usePathname()
|
const pathname = usePathname();
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "APB Desa",
|
label: "APB Desa",
|
||||||
value: "apbdesa",
|
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",
|
label: "Pendapatan",
|
||||||
value: "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",
|
label: "Belanja",
|
||||||
value: "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",
|
label: "Pembiayaan",
|
||||||
value: "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 handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find((t) => t.value === value);
|
||||||
if (tab) {
|
if (tab) {
|
||||||
router.push(tab.href)
|
router.push(tab.href);
|
||||||
}
|
}
|
||||||
setActiveTab(value)
|
setActiveTab(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const match = tabs.find(tab => tab.href === pathname)
|
const match = tabs.find((tab) => tab.href === pathname);
|
||||||
if (match) {
|
if (match) {
|
||||||
setActiveTab(match.value)
|
setActiveTab(match.value);
|
||||||
}
|
}
|
||||||
}, [pathname])
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Pendapatan Asli Desa</Title>
|
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
Pendapatan Asli Desa
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
</Title>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
<Tabs
|
||||||
))}
|
color={colors['blue-button']}
|
||||||
</TabsList>
|
variant="pills"
|
||||||
{tabs.map((e, i) => (
|
value={activeTab}
|
||||||
<TabsPanel key={i} value={e.value}>
|
onChange={handleTabChange}
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
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>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabs;
|
export default LayoutTabs;
|
||||||
|
|||||||
@@ -3,7 +3,19 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||||
import colors from '@/con/colors';
|
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 { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
@@ -23,7 +35,7 @@ function EditAPBDesa() {
|
|||||||
pembiayaanIds: apbState.update.form.pembiayaanIds || [],
|
pembiayaanIds: apbState.update.form.pembiayaanIds || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load APB desa by id saat pertama kali
|
// Load APB desa by id
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadAPBdesa = async () => {
|
const loadAPBdesa = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
@@ -46,12 +58,10 @@ function EditAPBDesa() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadAPBdesa();
|
loadAPBdesa();
|
||||||
}, [params?.id]); // ✅ hapus beritaState dari dependency
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update global state with form data
|
|
||||||
apbState.update.form = {
|
apbState.update.form = {
|
||||||
...apbState.update.form,
|
...apbState.update.form,
|
||||||
tahun: Number(formData.tahun),
|
tahun: Number(formData.tahun),
|
||||||
@@ -70,65 +80,95 @@ function EditAPBDesa() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
onClick={() => router.back()}
|
||||||
<Stack gap={"xs"}>
|
p="xs"
|
||||||
<Title order={3}>Edit APB Desa</Title>
|
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
|
<TextInput
|
||||||
type='number'
|
type="number"
|
||||||
value={formData.tahun}
|
value={formData.tahun}
|
||||||
onChange={(val) => {
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, tahun: val.target.value });
|
setFormData({ ...formData, tahun: e.target.value })
|
||||||
}}
|
}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
|
label={<Text fz="sm" fw="bold">Tahun</Text>}
|
||||||
placeholder="masukkan tahun"
|
placeholder="Masukkan tahun anggaran"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Selects */}
|
||||||
<SelectPendapatan
|
<SelectPendapatan
|
||||||
selectedIds={formData.pendapatanIds}
|
selectedIds={formData.pendapatanIds}
|
||||||
onSelectionChange={(ids) => {
|
onSelectionChange={(ids) =>
|
||||||
setFormData({ ...formData, pendapatanIds: ids });
|
setFormData({ ...formData, pendapatanIds: ids })
|
||||||
}}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectBelanja
|
<SelectBelanja
|
||||||
selectedIds={formData.belanjaIds}
|
selectedIds={formData.belanjaIds}
|
||||||
onSelectionChange={(ids) => {
|
onSelectionChange={(ids) =>
|
||||||
setFormData({ ...formData, belanjaIds: ids });
|
setFormData({ ...formData, belanjaIds: ids })
|
||||||
}}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectPembiayaan
|
<SelectPembiayaan
|
||||||
selectedIds={formData.pembiayaanIds}
|
selectedIds={formData.pembiayaanIds}
|
||||||
onSelectionChange={(ids) => {
|
onSelectionChange={(ids) =>
|
||||||
setFormData({ ...formData, pembiayaanIds: 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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* --- Sub Components --- */
|
||||||
|
|
||||||
/* Select Pendapatan */
|
function SelectPendapatan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
|
||||||
interface SelectPendapatanProps {
|
|
||||||
selectedIds: string[];
|
|
||||||
onSelectionChange: (ids: string[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectPendapatan({
|
|
||||||
selectedIds = [],
|
|
||||||
onSelectionChange,
|
|
||||||
}: SelectPendapatanProps) {
|
|
||||||
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
|
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pendapatanState.findMany.load().then(() => {
|
pendapatanState.findMany.load();
|
||||||
console.log("Pendapatan berhasil dimuat:", pendapatanState.findMany.data);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!pendapatanState.findMany.data) {
|
if (!pendapatanState.findMany.data) {
|
||||||
@@ -137,10 +177,10 @@ function EditAPBDesa() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Pendapatan</Text>}
|
label={<Text fz="sm" fw="bold">Pendapatan</Text>}
|
||||||
data={pendapatanState.findMany.data.map(p => ({
|
data={pendapatanState.findMany.data.map((p: any) => ({
|
||||||
value: p.id,
|
value: p.id,
|
||||||
label: p.name
|
label: p.name,
|
||||||
}))}
|
}))}
|
||||||
value={selectedIds}
|
value={selectedIds}
|
||||||
onChange={onSelectionChange}
|
onChange={onSelectionChange}
|
||||||
@@ -152,22 +192,11 @@ function EditAPBDesa() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Select Belanja */
|
function SelectBelanja({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
|
||||||
interface SelectBelanjaProps {
|
|
||||||
selectedIds: string[];
|
|
||||||
onSelectionChange: (ids: string[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectBelanja({
|
|
||||||
selectedIds = [],
|
|
||||||
onSelectionChange,
|
|
||||||
}: SelectBelanjaProps) {
|
|
||||||
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
|
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
belanjaState.findMany.load().then(() => {
|
belanjaState.findMany.load();
|
||||||
console.log("Belanja berhasil dimuat:", belanjaState.findMany.data);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!belanjaState.findMany.data) {
|
if (!belanjaState.findMany.data) {
|
||||||
@@ -176,10 +205,10 @@ function EditAPBDesa() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Belanja</Text>}
|
label={<Text fz="sm" fw="bold">Belanja</Text>}
|
||||||
data={belanjaState.findMany.data.map(b => ({
|
data={belanjaState.findMany.data.map((b: any) => ({
|
||||||
value: b.id,
|
value: b.id,
|
||||||
label: b.name
|
label: b.name,
|
||||||
}))}
|
}))}
|
||||||
value={selectedIds}
|
value={selectedIds}
|
||||||
onChange={onSelectionChange}
|
onChange={onSelectionChange}
|
||||||
@@ -191,22 +220,11 @@ function EditAPBDesa() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Select Pembiayaan */
|
function SelectPembiayaan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
|
||||||
interface SelectPembiayaanProps {
|
|
||||||
selectedIds: string[];
|
|
||||||
onSelectionChange: (ids: string[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectPembiayaan({
|
|
||||||
selectedIds = [],
|
|
||||||
onSelectionChange,
|
|
||||||
}: SelectPembiayaanProps) {
|
|
||||||
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
|
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pembiayaanState.findMany.load().then(() => {
|
pembiayaanState.findMany.load();
|
||||||
console.log("Pembiayaan berhasil dimuat:", pembiayaanState.findMany.data);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!pembiayaanState.findMany.data) {
|
if (!pembiayaanState.findMany.data) {
|
||||||
@@ -215,10 +233,10 @@ function EditAPBDesa() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Pembiayaan</Text>}
|
label={<Text fz="sm" fw="bold">Pembiayaan</Text>}
|
||||||
data={pembiayaanState.findMany.data.map(b => ({
|
data={pembiayaanState.findMany.data.map((p: any) => ({
|
||||||
value: b.id,
|
value: p.id,
|
||||||
label: b.name
|
label: p.name,
|
||||||
}))}
|
}))}
|
||||||
value={selectedIds}
|
value={selectedIds}
|
||||||
onChange={onSelectionChange}
|
onChange={onSelectionChange}
|
||||||
|
|||||||
@@ -2,148 +2,204 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||||
import colors from '@/con/colors';
|
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 { 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 { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function DetailAPBDesa() {
|
function DetailAPBDesa() {
|
||||||
const apbState = useProxy(PendapatanAsliDesa.ApbDesa)
|
const apbState = useProxy(PendapatanAsliDesa.ApbDesa);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
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) => {
|
const formatRupiah = (value: number) =>
|
||||||
return new Intl.NumberFormat('id-ID', {
|
new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
}).format(value);
|
}).format(value);
|
||||||
};
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
apbState.delete.byId(selectedId)
|
apbState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa")
|
router.push(
|
||||||
|
'/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!apbState.findUnique.data) {
|
if (!apbState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={40} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = apbState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
<Button
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
variant="subtle"
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
onClick={() => router.back()}
|
||||||
</Button>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
</Box>
|
mb={15}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
>
|
||||||
<Stack>
|
Kembali
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail APB Desa</Text>
|
</Button>
|
||||||
{apbState.findUnique.data ? (
|
|
||||||
<Paper key={apbState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
<Paper
|
||||||
<Stack gap={"xs"}>
|
withBorder
|
||||||
<Box>
|
w={{ base: '100%', md: '70%', lg: '50%' }}
|
||||||
<Text fw={"bold"} fz={"lg"}>Tahun</Text>
|
bg={colors['white-1']}
|
||||||
<Text fz={"lg"}>{apbState.findUnique.data?.tahun}</Text>
|
p="lg"
|
||||||
</Box>
|
radius="md"
|
||||||
<Box>
|
shadow="sm"
|
||||||
<Stack gap={"xs"}>
|
>
|
||||||
<Text fw={"bold"} fz={"lg"}>Detail Pembiayaan</Text>
|
<Stack gap="md">
|
||||||
{(apbState.findUnique.data?.pembiayaan || []).map((item) => (
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
<Text fz={"lg"} key={item.id}>
|
Detail APB Desa
|
||||||
{item.name}: {formatRupiah(Number(item.value))}
|
</Text>
|
||||||
</Text>
|
|
||||||
))}
|
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
|
||||||
<Text fz={"lg"} fw={"bold"}>
|
<Stack gap="sm">
|
||||||
Total: {formatRupiah((apbState.findUnique.data?.pembiayaan || [])
|
<Box>
|
||||||
.reduce((sum, item) => sum + Number(item.value), 0))}
|
<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>
|
</Text>
|
||||||
</Stack>
|
))}
|
||||||
</Box>
|
<Text fz="md" fw="bold">
|
||||||
<Box>
|
Total:{' '}
|
||||||
<Stack gap={"xs"}>
|
{formatRupiah(
|
||||||
<Text fw={"bold"} fz={"lg"}>Detail Belanja</Text>
|
(data?.pembiayaan || []).reduce(
|
||||||
{(apbState.findUnique.data?.belanja || []).map((item) => (
|
(sum, item) => sum + Number(item.value),
|
||||||
<Text fz={"lg"} key={item.id}>
|
0
|
||||||
{item.name}: {formatRupiah(Number(item.value))}
|
)
|
||||||
</Text>
|
)}
|
||||||
))}
|
</Text>
|
||||||
<Text fz={"lg"} fw={"bold"}>
|
</Stack>
|
||||||
Total: {formatRupiah((apbState.findUnique.data?.belanja || [])
|
</Box>
|
||||||
.reduce((sum, item) => sum + Number(item.value), 0))}
|
|
||||||
|
<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>
|
</Text>
|
||||||
</Stack>
|
))}
|
||||||
</Box>
|
<Text fz="md" fw="bold">
|
||||||
<Box>
|
Total:{' '}
|
||||||
<Stack gap={"xs"}>
|
{formatRupiah(
|
||||||
<Text fw={"bold"} fz={"lg"}>Detail Pendapatan</Text>
|
(data?.belanja || []).reduce(
|
||||||
{(apbState.findUnique.data?.pendapatan || []).map((item) => (
|
(sum, item) => sum + Number(item.value),
|
||||||
<Text fz={"lg"} key={item.id}>
|
0
|
||||||
{item.name}: {formatRupiah(Number(item.value))}
|
)
|
||||||
</Text>
|
)}
|
||||||
))}
|
</Text>
|
||||||
<Text fz={"lg"} fw={"bold"}>
|
</Stack>
|
||||||
Total: {formatRupiah((apbState.findUnique.data?.pendapatan || [])
|
</Box>
|
||||||
.reduce((sum, item) => sum + Number(item.value), 0))}
|
|
||||||
|
<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>
|
</Text>
|
||||||
</Stack>
|
))}
|
||||||
</Box>
|
<Text fz="md" fw="bold">
|
||||||
<Flex gap={"xs"} mt={10}>
|
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
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (apbState.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(apbState.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={apbState.delete.loading || !apbState.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit APB Desa" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (apbState.findUnique.data) {
|
onClick={() =>
|
||||||
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${apbState.findUnique.data.id}/edit`);
|
router.push(
|
||||||
}
|
`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${data.id}/edit`
|
||||||
}}
|
)
|
||||||
disabled={!apbState.findUnique.data}
|
}
|
||||||
color={"green"}
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus APB Desa ini?'
|
text="Apakah Anda yakin ingin menghapus APB Desa ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||||
import colors from '@/con/colors';
|
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 { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function CreateAPBDesa() {
|
function CreateAPBDesa() {
|
||||||
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
|
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
apbDesaState.create.form = {
|
apbDesaState.create.form = {
|
||||||
@@ -17,74 +30,101 @@ function CreateAPBDesa() {
|
|||||||
pendapatanIds: [],
|
pendapatanIds: [],
|
||||||
belanjaIds: [],
|
belanjaIds: [],
|
||||||
pembiayaanIds: [],
|
pembiayaanIds: [],
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await apbDesaState.create.submit()
|
await apbDesaState.create.submit();
|
||||||
resetForm()
|
resetForm();
|
||||||
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa")
|
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
</Button>
|
||||||
<Stack gap={"xs"}>
|
</Tooltip>
|
||||||
<Title order={3}>Create APB Desa</Title>
|
<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
|
<TextInput
|
||||||
type='number'
|
type="number"
|
||||||
value={apbDesaState.create.form.tahun}
|
value={apbDesaState.create.form.tahun}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
apbDesaState.create.form.tahun = Number(val.target.value);
|
apbDesaState.create.form.tahun = Number(val.target.value);
|
||||||
}}
|
}}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
|
label={<Text fz="sm" fw="bold">Tahun</Text>}
|
||||||
placeholder="masukkan tahun"
|
placeholder="Masukkan tahun anggaran"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectPendapatan
|
<SelectPendapatan
|
||||||
selectedIds={apbDesaState.create.form.pendapatanIds}
|
selectedIds={apbDesaState.create.form.pendapatanIds}
|
||||||
onSelectionChange={(ids) => {
|
onSelectionChange={(ids) => {
|
||||||
apbDesaState.create.form.pendapatanIds = ids;
|
apbDesaState.create.form.pendapatanIds = ids;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectBelanja
|
<SelectBelanja
|
||||||
selectedIds={apbDesaState.create.form.belanjaIds}
|
selectedIds={apbDesaState.create.form.belanjaIds}
|
||||||
onSelectionChange={(ids) => {
|
onSelectionChange={(ids) => {
|
||||||
apbDesaState.create.form.belanjaIds = ids;
|
apbDesaState.create.form.belanjaIds = ids;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectPembiayaan
|
<SelectPembiayaan
|
||||||
selectedIds={apbDesaState.create.form.pembiayaanIds}
|
selectedIds={apbDesaState.create.form.pembiayaanIds}
|
||||||
onSelectionChange={(ids) => {
|
onSelectionChange={(ids) => {
|
||||||
apbDesaState.create.form.pembiayaanIds = 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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Select Pendapatan */
|
/* ---------- Select Pendapatan ---------- */
|
||||||
interface SelectPendapatanProps {
|
interface SelectPendapatanProps {
|
||||||
selectedIds: string[];
|
selectedIds: string[];
|
||||||
onSelectionChange: (ids: string[]) => void;
|
onSelectionChange: (ids: string[]) => void;
|
||||||
}
|
}
|
||||||
|
function SelectPendapatan({ selectedIds = [], onSelectionChange }: SelectPendapatanProps) {
|
||||||
function SelectPendapatan({
|
|
||||||
selectedIds = [],
|
|
||||||
onSelectionChange,
|
|
||||||
}: SelectPendapatanProps) {
|
|
||||||
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
|
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pendapatanState.findMany.load().then(() => {
|
pendapatanState.findMany.load();
|
||||||
console.log("Pendapatan berhasil dimuat:", pendapatanState.findMany.data);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!pendapatanState.findMany.data) {
|
if (!pendapatanState.findMany.data) {
|
||||||
@@ -93,10 +133,10 @@ function CreateAPBDesa() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Pendapatan</Text>}
|
label={<Text fz="sm" fw="bold">Pendapatan</Text>}
|
||||||
data={pendapatanState.findMany.data.map(p => ({
|
data={pendapatanState.findMany.data.map((p) => ({
|
||||||
value: p.id,
|
value: p.id,
|
||||||
label: p.name
|
label: p.name,
|
||||||
}))}
|
}))}
|
||||||
value={selectedIds}
|
value={selectedIds}
|
||||||
onChange={onSelectionChange}
|
onChange={onSelectionChange}
|
||||||
@@ -108,22 +148,16 @@ function CreateAPBDesa() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Select Belanja */
|
/* ---------- Select Belanja ---------- */
|
||||||
interface SelectBelanjaProps {
|
interface SelectBelanjaProps {
|
||||||
selectedIds: string[];
|
selectedIds: string[];
|
||||||
onSelectionChange: (ids: string[]) => void;
|
onSelectionChange: (ids: string[]) => void;
|
||||||
}
|
}
|
||||||
|
function SelectBelanja({ selectedIds = [], onSelectionChange }: SelectBelanjaProps) {
|
||||||
function SelectBelanja({
|
|
||||||
selectedIds = [],
|
|
||||||
onSelectionChange,
|
|
||||||
}: SelectBelanjaProps) {
|
|
||||||
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
|
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
belanjaState.findMany.load().then(() => {
|
belanjaState.findMany.load();
|
||||||
console.log("Belanja berhasil dimuat:", belanjaState.findMany.data);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!belanjaState.findMany.data) {
|
if (!belanjaState.findMany.data) {
|
||||||
@@ -132,10 +166,10 @@ function CreateAPBDesa() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Belanja</Text>}
|
label={<Text fz="sm" fw="bold">Belanja</Text>}
|
||||||
data={belanjaState.findMany.data.map(b => ({
|
data={belanjaState.findMany.data.map((b) => ({
|
||||||
value: b.id,
|
value: b.id,
|
||||||
label: b.name
|
label: b.name,
|
||||||
}))}
|
}))}
|
||||||
value={selectedIds}
|
value={selectedIds}
|
||||||
onChange={onSelectionChange}
|
onChange={onSelectionChange}
|
||||||
@@ -147,22 +181,16 @@ function CreateAPBDesa() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Select Pembiayaan */
|
/* ---------- Select Pembiayaan ---------- */
|
||||||
interface SelectPembiayaanProps {
|
interface SelectPembiayaanProps {
|
||||||
selectedIds: string[];
|
selectedIds: string[];
|
||||||
onSelectionChange: (ids: string[]) => void;
|
onSelectionChange: (ids: string[]) => void;
|
||||||
}
|
}
|
||||||
|
function SelectPembiayaan({ selectedIds = [], onSelectionChange }: SelectPembiayaanProps) {
|
||||||
function SelectPembiayaan({
|
|
||||||
selectedIds = [],
|
|
||||||
onSelectionChange,
|
|
||||||
}: SelectPembiayaanProps) {
|
|
||||||
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
|
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pembiayaanState.findMany.load().then(() => {
|
pembiayaanState.findMany.load();
|
||||||
console.log("Pembiayaan berhasil dimuat:", pembiayaanState.findMany.data);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!pembiayaanState.findMany.data) {
|
if (!pembiayaanState.findMany.data) {
|
||||||
@@ -171,10 +199,10 @@ function CreateAPBDesa() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Pembiayaan</Text>}
|
label={<Text fz="sm" fw="bold">Pembiayaan</Text>}
|
||||||
data={pembiayaanState.findMany.data.map(b => ({
|
data={pembiayaanState.findMany.data.map((b) => ({
|
||||||
value: b.id,
|
value: b.id,
|
||||||
label: b.name
|
label: b.name,
|
||||||
}))}
|
}))}
|
||||||
value={selectedIds}
|
value={selectedIds}
|
||||||
onChange={onSelectionChange}
|
onChange={onSelectionChange}
|
||||||
|
|||||||
@@ -1,23 +1,38 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { 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 { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
|
||||||
|
|
||||||
|
|
||||||
function APBDesa() {
|
function APBDesa() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='APB Desa'
|
title="APB Desa"
|
||||||
placeholder='pencarian'
|
placeholder="Cari tahun atau nominal..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -28,77 +43,147 @@ function APBDesa() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListAPBDesa({ search }: { search: string }) {
|
function ListAPBDesa({ search }: { search: string }) {
|
||||||
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
|
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const formatRupiah = (value: number) => {
|
const {
|
||||||
return new Intl.NumberFormat('id-ID', {
|
data,
|
||||||
style: 'currency',
|
page,
|
||||||
currency: 'IDR',
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = apbDesaState.findMany;
|
||||||
|
|
||||||
|
const formatRupiah = (value: number) =>
|
||||||
|
new Intl.NumberFormat("id-ID", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "IDR",
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
}).format(value);
|
}).format(value);
|
||||||
};
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
apbDesaState.findMany.load();
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (apbDesaState.findMany.data || []).filter(item => {
|
const filteredData = data || [];
|
||||||
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)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!apbDesaState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors["white-1"]} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List APB Desa'
|
<Text fw={600} fz="lg">
|
||||||
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create'
|
List APB Desa
|
||||||
/>
|
</Text>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Tooltip label="Tambah APB Desa" withArrow>
|
||||||
<TableThead>
|
<Button
|
||||||
<TableTr>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableTh>Tahun</TableTh>
|
color="blue"
|
||||||
<TableTh>Pembiayaan</TableTh>
|
variant="light"
|
||||||
<TableTh>Belanja</TableTh>
|
onClick={() =>
|
||||||
<TableTh>Pendapatan</TableTh>
|
router.push(
|
||||||
<TableTh>Detail</TableTh>
|
"/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create"
|
||||||
</TableTr>
|
)
|
||||||
</TableThead>
|
}
|
||||||
<TableTbody>
|
>
|
||||||
{filteredData.map((item) => (
|
Tambah Baru
|
||||||
<TableTr key={item.id}>
|
</Button>
|
||||||
<TableTd>{item.tahun}</TableTd>
|
</Tooltip>
|
||||||
<TableTd>{formatRupiah(item.pembiayaan.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd>
|
</Group>
|
||||||
<TableTd>{formatRupiah(item.belanja.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<TableTd>{formatRupiah(item.pendapatan.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd>
|
<Table highlightOnHover>
|
||||||
<TableTd>
|
<TableThead>
|
||||||
<Button
|
<TableTr>
|
||||||
bg={"green"}
|
<TableTh style={{ width: "15%" }}>Tahun</TableTh>
|
||||||
onClick={() =>
|
<TableTh style={{ width: "25%" }}>Pembiayaan</TableTh>
|
||||||
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`)
|
<TableTh style={{ width: "25%" }}>Belanja</TableTh>
|
||||||
}
|
<TableTh style={{ width: "25%" }}>Pendapatan</TableTh>
|
||||||
>
|
<TableTh style={{ width: "10%" }}>Aksi</TableTh>
|
||||||
<IconDeviceImacCog size={25} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
</TableTbody>
|
<TableTbody>
|
||||||
</Table>
|
{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>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -72,41 +81,72 @@ function EditBelanja() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
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'}>
|
{/* Card */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Edit Jenis Pendapatan</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
<TextInput
|
bg={colors['white-1']}
|
||||||
value={formData.name}
|
p="lg"
|
||||||
onChange={(val) => {
|
radius="md"
|
||||||
setFormData({ ...formData, name: val.target.value });
|
shadow="sm"
|
||||||
}}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>}
|
>
|
||||||
placeholder='Masukkan nama Jenis Pendapatan'
|
<Stack gap="md">
|
||||||
/>
|
<TextInput
|
||||||
<TextInput
|
label="Nama Jenis Belanja"
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
|
placeholder="Masukkan nama jenis belanja"
|
||||||
placeholder='Masukkan nilai'
|
value={formData.name}
|
||||||
value={formatRupiah(formData.value)}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
onChange={(val) => {
|
required
|
||||||
const raw = val.currentTarget.value;
|
/>
|
||||||
const cleanValue = unformatRupiah(raw);
|
|
||||||
setFormData({ ...formData, value: cleanValue });
|
<TextInput
|
||||||
}}
|
label="Nilai"
|
||||||
/>
|
placeholder="Masukkan nilai"
|
||||||
<Group>
|
value={formatRupiah(formData.value)}
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
onChange={(e) => {
|
||||||
</Group>
|
const raw = e.currentTarget.value;
|
||||||
</Stack>
|
const cleanValue = unformatRupiah(raw);
|
||||||
</Paper>
|
setFormData({ ...formData, value: cleanValue });
|
||||||
</Box>
|
}}
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
function CreateBelanja() {
|
function CreateBelanja() {
|
||||||
const belanjaState = useProxy(PendapatanAsliDesa.belanja)
|
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const formatRupiah = (value: number | string) => {
|
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', {
|
return new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
@@ -25,48 +38,83 @@ function CreateBelanja() {
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
belanjaState.create.form = {
|
belanjaState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
value: 0,
|
value: 0,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await belanjaState.create.submit();
|
if (!belanjaState.create.form.name || !belanjaState.create.form.value) {
|
||||||
resetForm()
|
return toast.warn('Lengkapi semua field terlebih dahulu');
|
||||||
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>
|
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
await belanjaState.create.submit();
|
||||||
<Stack gap={"xs"}>
|
resetForm();
|
||||||
<Title order={4}>Create Jenis Belanja</Title>
|
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
|
<TextInput
|
||||||
|
label={<Text fw="bold" fz="sm">Nama Jenis Belanja</Text>}
|
||||||
|
placeholder="Masukkan nama jenis belanja"
|
||||||
value={belanjaState.create.form.name}
|
value={belanjaState.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => (belanjaState.create.form.name = e.target.value)}
|
||||||
belanjaState.create.form.name = val.target.value;
|
required
|
||||||
}}
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Belanja</Text>}
|
|
||||||
placeholder='Masukkan nama jenis belanja'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
type='text'
|
type="text"
|
||||||
|
label={<Text fw="bold" fz="sm">Nilai</Text>}
|
||||||
|
placeholder="Masukkan nilai belanja"
|
||||||
value={formatRupiah(belanjaState.create.form.value)}
|
value={formatRupiah(belanjaState.create.form.value)}
|
||||||
onChange={(val) => {
|
onChange={(e) => {
|
||||||
const raw = val.currentTarget.value;
|
const raw = e.currentTarget.value;
|
||||||
const cleanValue = unformatRupiah(raw);
|
belanjaState.create.form.value = unformatRupiah(raw);
|
||||||
belanjaState.create.form.value = cleanValue;
|
|
||||||
}}
|
}}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
|
required
|
||||||
placeholder='Masukkan nilai'
|
|
||||||
/>
|
/>
|
||||||
<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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,24 +1,41 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
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 { 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 { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../../_com/header';
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
|
||||||
import HeaderSearch from '../../../_com/header';
|
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
|
|
||||||
|
|
||||||
function Belanja() {
|
function Belanja() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Belanja'
|
title="Belanja"
|
||||||
placeholder='pencarian'
|
placeholder="Cari belanja berdasarkan nama atau nilai..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -29,108 +46,175 @@ function Belanja() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListBelanja({ search }: { search: string }) {
|
function ListBelanja({ search }: { search: string }) {
|
||||||
const belanjaState = useProxy(PendapatanAsliDesa.belanja)
|
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
const formatRupiah = (value: number) => {
|
const {
|
||||||
return new Intl.NumberFormat('id-ID', {
|
data,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
} = belanjaState.findMany;
|
||||||
|
|
||||||
|
const formatRupiah = (value: number) =>
|
||||||
|
new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
}).format(value);
|
}).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 = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
belanjaState.delete.byId(selectedId)
|
belanjaState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
belanjaState.findMany.load()
|
load(page, 10, search);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
belanjaState.findMany.load();
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (belanjaState.findMany.data || []).filter(item => {
|
const filteredData = data || [];
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword) ||
|
|
||||||
item.value.toString().toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!belanjaState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Belanja'
|
<Title order={4}>Daftar Belanja</Title>
|
||||||
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create'
|
<Tooltip label="Tambah Belanja" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Nama</TableTh>
|
onClick={() =>
|
||||||
<TableTh>Nilai</TableTh>
|
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create')
|
||||||
<TableTh>Persentase</TableTh>
|
}
|
||||||
<TableTh>Edit</TableTh>
|
>
|
||||||
<TableTh>Delete</TableTh>
|
Tambah Baru
|
||||||
</TableTr>
|
</Button>
|
||||||
</TableThead>
|
</Tooltip>
|
||||||
<TableTbody>
|
</Group>
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<TableTd>{item.name}</TableTd>
|
<Table highlightOnHover striped withTableBorder withRowBorders>
|
||||||
<TableTd>{formatRupiah(item.value)}</TableTd>
|
<TableThead>
|
||||||
<TableTd>{((item.value / totalBelanja) * 100).toFixed(0)}%</TableTd>
|
<TableTr>
|
||||||
<TableTd>
|
<TableTh>Nama</TableTh>
|
||||||
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`)}>
|
<TableTh>Nilai</TableTh>
|
||||||
<IconEdit size={20} />
|
<TableTh>Persentase</TableTh>
|
||||||
</Button>
|
<TableTh>Aksi</TableTh>
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button
|
|
||||||
color='red'
|
|
||||||
disabled={belanjaState.delete.loading}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedId(item.id)
|
|
||||||
setModalHapus(true)
|
|
||||||
}}>
|
|
||||||
<IconTrash size={20} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
<TableTr>
|
<TableTbody>
|
||||||
<TableTd colSpan={4}>
|
{filteredData.length > 0 ? (
|
||||||
<Text fw={'bold'}>Total</Text>
|
<>
|
||||||
</TableTd>
|
{filteredData.map((item) => (
|
||||||
<TableTd>
|
<TableTr key={item.id}>
|
||||||
{formatRupiah(belanjaState.findMany.data.reduce((total, item) => total + item.value, 0))}
|
<TableTd>
|
||||||
</TableTd>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
</TableTr>
|
{item.name}
|
||||||
</TableTbody>
|
</Text>
|
||||||
</Table>
|
</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>
|
</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 */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
text='Apakah anda yakin ingin menghapus belanja ini?'
|
text="Apakah anda yakin ingin menghapus belanja ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -20,7 +29,7 @@ function EditPembiayaan() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formatRupiah = (value: number | string) => {
|
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', {
|
return new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
@@ -46,8 +55,8 @@ function EditPembiayaan() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading pembiayaan:", error);
|
console.error('Error loading pembiayaan:', error);
|
||||||
toast.error("Gagal memuat data pembiayaan");
|
toast.error('Gagal memuat data pembiayaan');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,53 +69,84 @@ function EditPembiayaan() {
|
|||||||
...pembiayaanState.update.form,
|
...pembiayaanState.update.form,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
value: Number(formData.value),
|
value: Number(formData.value),
|
||||||
}
|
};
|
||||||
|
|
||||||
await pembiayaanState.update.update();
|
await pembiayaanState.update.update();
|
||||||
toast.success("Jenis Pembiayaan berhasil diperbarui!");
|
toast.success('Jenis Pembiayaan berhasil diperbarui!');
|
||||||
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan");
|
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating jenis pembiayaan:", error);
|
console.error('Error updating jenis pembiayaan:', error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui jenis pembiayaan");
|
toast.error('Terjadi kesalahan saat memperbarui jenis pembiayaan');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan Back Button */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
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'}>
|
{/* Card Form */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Edit Jenis Pembiayaan</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
<TextInput
|
bg={colors['white-1']}
|
||||||
value={formData.name}
|
p="lg"
|
||||||
onChange={(val) => {
|
radius="md"
|
||||||
setFormData({ ...formData, name: val.target.value });
|
shadow="sm"
|
||||||
}}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pembiayaan</Text>}
|
>
|
||||||
placeholder='Masukkan nama Jenis Pembiayaan'
|
<Stack gap="md">
|
||||||
/>
|
<TextInput
|
||||||
<TextInput
|
label="Nama Jenis Pembiayaan"
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
|
placeholder="Masukkan nama jenis pembiayaan"
|
||||||
placeholder='Masukkan nilai'
|
value={formData.name}
|
||||||
value={formatRupiah(formData.value)}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
onChange={(val) => {
|
required
|
||||||
const raw = val.currentTarget.value;
|
/>
|
||||||
const cleanValue = unformatRupiah(raw);
|
|
||||||
setFormData({ ...formData, value: cleanValue });
|
<TextInput
|
||||||
}}
|
label="Nilai"
|
||||||
/>
|
placeholder="Masukkan nilai"
|
||||||
<Group>
|
value={formatRupiah(formData.value)}
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
onChange={(e) => {
|
||||||
</Group>
|
const raw = e.currentTarget.value;
|
||||||
</Stack>
|
const cleanValue = unformatRupiah(raw);
|
||||||
</Paper>
|
setFormData({ ...formData, value: cleanValue });
|
||||||
</Box>
|
}}
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
function CreatePembiayaan() {
|
function CreatePembiayaan() {
|
||||||
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan)
|
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const formatRupiah = (value: number | string) => {
|
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', {
|
return new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
@@ -26,48 +38,85 @@ function CreatePembiayaan() {
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
pembiayaanState.create.form = {
|
pembiayaanState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
value: 0,
|
value: 0,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await pembiayaanState.create.submit();
|
if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) {
|
||||||
resetForm()
|
return toast.warn('Nama dan nilai wajib diisi');
|
||||||
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>
|
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
await pembiayaanState.create.submit();
|
||||||
<Stack gap={"xs"}>
|
resetForm();
|
||||||
<Title order={4}>Create Jenis Pembiayaan</Title>
|
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
|
<TextInput
|
||||||
|
label={<Text fw="bold" fz="sm">Nama Jenis Pembiayaan</Text>}
|
||||||
|
placeholder="Masukkan nama jenis pembiayaan"
|
||||||
value={pembiayaanState.create.form.name}
|
value={pembiayaanState.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(e) => {
|
||||||
pembiayaanState.create.form.name = val.target.value;
|
pembiayaanState.create.form.name = e.currentTarget.value;
|
||||||
}}
|
}}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pembiayaan</Text>}
|
required
|
||||||
placeholder='Masukkan nama jenis pembiayaan'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
type='text'
|
type="text"
|
||||||
|
label={<Text fw="bold" fz="sm">Nilai</Text>}
|
||||||
|
placeholder="Masukkan nilai"
|
||||||
value={formatRupiah(pembiayaanState.create.form.value)}
|
value={formatRupiah(pembiayaanState.create.form.value)}
|
||||||
onChange={(val) => {
|
onChange={(e) => {
|
||||||
const raw = val.currentTarget.value;
|
const raw = e.currentTarget.value;
|
||||||
const cleanValue = unformatRupiah(raw);
|
pembiayaanState.create.form.value = unformatRupiah(raw);
|
||||||
pembiayaanState.create.form.value = cleanValue;
|
|
||||||
}}
|
}}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
|
required
|
||||||
placeholder='Masukkan nilai'
|
|
||||||
/>
|
/>
|
||||||
<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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,14 +1,31 @@
|
|||||||
'use client'
|
'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 React, { useState } from 'react';
|
||||||
import HeaderSearch from '../../../_com/header';
|
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 PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
|
||||||
function Pembiayaan() {
|
function Pembiayaan() {
|
||||||
@@ -16,8 +33,8 @@ function Pembiayaan() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Pembiayaan'
|
title="Pembiayaan"
|
||||||
placeholder='pencarian'
|
placeholder="Cari nama pembiayaan..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -28,111 +45,171 @@ function Pembiayaan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListPembiayaan({ search }: { search: string }) {
|
function ListPembiayaan({ search }: { search: string }) {
|
||||||
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan)
|
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
const formatRupiah = (value: number) => {
|
const {
|
||||||
return new Intl.NumberFormat('id-ID', {
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = pembiayaanState.findMany;
|
||||||
|
|
||||||
|
const formatRupiah = (value: number) =>
|
||||||
|
new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
}).format(value);
|
}).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 = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
pembiayaanState.delete.byId(selectedId)
|
pembiayaanState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
pembiayaanState.findMany.load()
|
load(page, 10, search);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pembiayaanState.findMany.load();
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (pembiayaanState.findMany.data || []).filter(item => {
|
if (loading || !data) {
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword) ||
|
|
||||||
item.value.toString().toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!pembiayaanState.findMany.data) {
|
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredData = data || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Pembiayaan'
|
<Title order={4}>Daftar Pembiayaan</Title>
|
||||||
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create'
|
<Tooltip label="Tambah Pembiayaan" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Nama</TableTh>
|
onClick={() =>
|
||||||
<TableTh>Nilai</TableTh>
|
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create')
|
||||||
<TableTh>Persentase</TableTh>
|
}
|
||||||
<TableTh>Edit</TableTh>
|
>
|
||||||
<TableTh>Delete</TableTh>
|
Tambah Baru
|
||||||
</TableTr>
|
</Button>
|
||||||
</TableThead>
|
</Tooltip>
|
||||||
<TableTbody>
|
</Group>
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<TableTd>{item.name}</TableTd>
|
<Table highlightOnHover striped withTableBorder withRowBorders>
|
||||||
<TableTd>{formatRupiah(item.value)}</TableTd>
|
<TableThead>
|
||||||
<TableTd>{((item.value / totalPembiayaan) * 100).toFixed(0)}%</TableTd>
|
<TableTr>
|
||||||
<TableTd>
|
<TableTh>Nama</TableTh>
|
||||||
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/${item.id}`)}>
|
<TableTh>Nilai</TableTh>
|
||||||
<IconEdit size={20} />
|
<TableTh>Persentase</TableTh>
|
||||||
</Button>
|
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button
|
|
||||||
color='red'
|
|
||||||
disabled={pembiayaanState.delete.loading}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedId(item.id)
|
|
||||||
setModalHapus(true)
|
|
||||||
}}>
|
|
||||||
<IconTrash size={20} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
<TableTr>
|
<TableTbody>
|
||||||
<TableTd colSpan={4}>
|
{filteredData.length > 0 ? (
|
||||||
<Text fw={'bold'}>Total</Text>
|
<>
|
||||||
</TableTd>
|
{filteredData.map((item) => (
|
||||||
<TableTd>
|
<TableTr key={item.id}>
|
||||||
{formatRupiah(pembiayaanState.findMany.data.reduce((total, item) => total + item.value, 0))}
|
<TableTd>
|
||||||
</TableTd>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
</TableTr>
|
{item.name}
|
||||||
</TableTbody>
|
</Text>
|
||||||
</Table>
|
</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>
|
</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 */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
text='Apakah anda yakin ingin menghapus pembiayaan ini?'
|
text="Apakah anda yakin ingin menghapus pembiayaan ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Pembiayaan;
|
export default Pembiayaan;
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -20,7 +29,7 @@ function EditPendapatan() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formatRupiah = (value: number | string) => {
|
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', {
|
return new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
@@ -28,15 +37,13 @@ function EditPendapatan() {
|
|||||||
}).format(number);
|
}).format(number);
|
||||||
};
|
};
|
||||||
|
|
||||||
const unformatRupiah = (value: string) => {
|
const unformatRupiah = (value: string) => Number(value.replace(/\D/g, ''));
|
||||||
return Number(value.replace(/\D/g, ''));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPendapatan = async () => {
|
const id = params?.id as string;
|
||||||
const id = params?.id as string;
|
if (!id) return;
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
|
const loadPendapatan = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await pendapatanState.update.load(id);
|
const data = await pendapatanState.update.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -46,8 +53,8 @@ function EditPendapatan() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading pendapatan:", error);
|
console.error('Error loading pendapatan:', error);
|
||||||
toast.error("Gagal memuat data pendapatan");
|
toast.error('Gagal memuat data pendapatan');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,53 +67,84 @@ function EditPendapatan() {
|
|||||||
...pendapatanState.update.form,
|
...pendapatanState.update.form,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
value: Number(formData.value),
|
value: Number(formData.value),
|
||||||
}
|
};
|
||||||
|
|
||||||
await pendapatanState.update.update();
|
await pendapatanState.update.update();
|
||||||
toast.success("Jenis Pendapatan berhasil diperbarui!");
|
toast.success('Jenis Pendapatan berhasil diperbarui!');
|
||||||
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan");
|
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating jenis pendapatan:", error);
|
console.error('Error updating jenis pendapatan:', error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui jenis pendapatan");
|
toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header with Back Button */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
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'}>
|
{/* Card Form */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Edit Jenis Pendapatan</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
<TextInput
|
bg={colors['white-1']}
|
||||||
value={formData.name}
|
p="lg"
|
||||||
onChange={(val) => {
|
radius="md"
|
||||||
setFormData({ ...formData, name: val.target.value });
|
shadow="sm"
|
||||||
}}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>}
|
>
|
||||||
placeholder='Masukkan nama Jenis Pendapatan'
|
<Stack gap="md">
|
||||||
/>
|
<TextInput
|
||||||
<TextInput
|
label="Nama Jenis Pendapatan"
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
|
placeholder="Masukkan nama jenis pendapatan"
|
||||||
placeholder='Masukkan nilai'
|
value={formData.name}
|
||||||
value={formatRupiah(formData.value)}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
onChange={(val) => {
|
required
|
||||||
const raw = val.currentTarget.value;
|
/>
|
||||||
const cleanValue = unformatRupiah(raw);
|
|
||||||
setFormData({ ...formData, value: cleanValue });
|
<TextInput
|
||||||
}}
|
label="Nilai"
|
||||||
/>
|
placeholder="Masukkan nilai"
|
||||||
<Group>
|
value={formatRupiah(formData.value)}
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
onChange={(e) => {
|
||||||
</Group>
|
const raw = e.currentTarget.value;
|
||||||
</Stack>
|
const cleanValue = unformatRupiah(raw);
|
||||||
</Paper>
|
setFormData({ ...formData, value: cleanValue });
|
||||||
</Box>
|
}}
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function CreatePendapatan() {
|
function CreatePendapatan() {
|
||||||
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan)
|
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const formatRupiah = (value: number | string) => {
|
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, ''));
|
||||||
@@ -25,48 +35,90 @@ function CreatePendapatan() {
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
pendapatanState.create.form = {
|
pendapatanState.create.form = {
|
||||||
name: "",
|
name: '',
|
||||||
value: 0,
|
value: 0,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await pendapatanState.create.submit();
|
await pendapatanState.create.submit();
|
||||||
resetForm()
|
resetForm();
|
||||||
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan")
|
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>
|
|
||||||
|
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
return (
|
||||||
<Stack gap={"xs"}>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Title order={4}>Create Jenis Pendapatan</Title>
|
{/* 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
|
<TextInput
|
||||||
value={pendapatanState.create.form.name}
|
value={pendapatanState.create.form.name}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
pendapatanState.create.form.name = val.target.value;
|
pendapatanState.create.form.name = val.target.value;
|
||||||
}}
|
}}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>}
|
label={
|
||||||
placeholder='Masukkan nama jenis pendapatan'
|
<Text fw="bold" fz="sm">
|
||||||
|
Nama Jenis Pendapatan
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
placeholder="Masukkan nama jenis pendapatan"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
type='text'
|
type="text"
|
||||||
value={formatRupiah(pendapatanState.create.form.value)}
|
value={formatRupiah(pendapatanState.create.form.value)}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
const raw = val.currentTarget.value;
|
const raw = val.currentTarget.value;
|
||||||
const cleanValue = unformatRupiah(raw);
|
const cleanValue = unformatRupiah(raw);
|
||||||
pendapatanState.create.form.value = cleanValue;
|
pendapatanState.create.form.value = cleanValue;
|
||||||
}}
|
}}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
|
label={
|
||||||
placeholder='Masukkan nilai'
|
<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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
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 { 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 { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../../_com/header';
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
|
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
|
||||||
import HeaderSearch from '../../../_com/header';
|
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
|
|
||||||
|
|
||||||
function Pendapatan() {
|
function Pendapatan() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -18,7 +35,7 @@ function Pendapatan() {
|
|||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Pendapatan'
|
title='Pendapatan'
|
||||||
placeholder='pencarian'
|
placeholder='Cari pendapatan...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -29,105 +46,166 @@ function Pendapatan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListPendapatan({ search }: { search: string }) {
|
function ListPendapatan({ search }: { search: string }) {
|
||||||
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan)
|
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
const formatRupiah = (value: number) => {
|
const {
|
||||||
return new Intl.NumberFormat('id-ID', {
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = pendapatanState.findMany;
|
||||||
|
|
||||||
|
const formatRupiah = (value: number) =>
|
||||||
|
new Intl.NumberFormat('id-ID', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'IDR',
|
currency: 'IDR',
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
}).format(value);
|
}).format(value);
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
pendapatanState.delete.byId(selectedId)
|
pendapatanState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
pendapatanState.findMany.load()
|
load(page, 10, search);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pendapatanState.findMany.load();
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (pendapatanState.findMany.data || []).filter(item => {
|
const filteredData = data || [];
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.name.toLowerCase().includes(keyword) ||
|
|
||||||
item.value.toString().toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!pendapatanState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalValue = filteredData.reduce((total, item) => total + item.value, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Pendapatan'
|
<Title order={4}>Daftar Pendapatan</Title>
|
||||||
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create'
|
<Tooltip label="Tambah Pendapatan Baru" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Nama</TableTh>
|
onClick={() =>
|
||||||
<TableTh>Nilai</TableTh>
|
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create')
|
||||||
<TableTh>Edit</TableTh>
|
}
|
||||||
<TableTh>Delete</TableTh>
|
>
|
||||||
</TableTr>
|
Tambah Baru
|
||||||
</TableThead>
|
</Button>
|
||||||
<TableTbody>
|
</Tooltip>
|
||||||
{filteredData.map((item) => (
|
</Group>
|
||||||
<TableTr key={item.id}>
|
|
||||||
<TableTd>{item.name}</TableTd>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<TableTd>{formatRupiah(item.value)}</TableTd>
|
<Table highlightOnHover striped withTableBorder withRowBorders>
|
||||||
<TableTd>
|
<TableThead>
|
||||||
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)}>
|
<TableTr>
|
||||||
<IconEdit size={20} />
|
<TableTh style={{ width: '40%' }}>Nama</TableTh>
|
||||||
</Button>
|
<TableTh style={{ width: '25%' }}>Nilai</TableTh>
|
||||||
</TableTd>
|
<TableTh style={{ width: '15%' }}>Edit</TableTh>
|
||||||
<TableTd>
|
<TableTh style={{ width: '15%' }}>Delete</TableTh>
|
||||||
<Button
|
|
||||||
color='red'
|
|
||||||
disabled={pendapatanState.delete.loading}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedId(item.id)
|
|
||||||
setModalHapus(true)
|
|
||||||
}}>
|
|
||||||
<IconTrash size={20} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
<TableTr>
|
<TableTbody>
|
||||||
<TableTd colSpan={4}>
|
{filteredData.length > 0 ? (
|
||||||
<Text fw={'bold'}>Total</Text>
|
<>
|
||||||
</TableTd>
|
{filteredData.map((item) => (
|
||||||
<TableTd>
|
<TableTr key={item.id}>
|
||||||
{formatRupiah(pendapatanState.findMany.data.reduce((total, item) => total + item.value, 0))}
|
<TableTd>
|
||||||
</TableTd>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
</TableTr>
|
{item.name}
|
||||||
</TableTbody>
|
</Text>
|
||||||
</Table>
|
</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>
|
</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 */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
text='Apakah anda yakin ingin menghapus pendapatan ini?'
|
text="Apakah anda yakin ingin menghapus pendapatan ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function CreateJumlahPendudukMiskin() {
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateJPM.create.form = {
|
stateJPM.create.form = {
|
||||||
year: 0,
|
year: new Date().getFullYear(), // Default to current year
|
||||||
totalPoorPopulation: 0,
|
totalPoorPopulation: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,10 +48,11 @@ function CreateJumlahPendudukMiskin() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
label="Tahun"
|
label="Tahun"
|
||||||
type="number"
|
type="number"
|
||||||
value={stateJPM.create.form.year}
|
value={stateJPM.create.form.year || ''}
|
||||||
placeholder="Masukkan tahun"
|
placeholder="Masukkan tahun"
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
stateJPM.create.form.year = Number(val.currentTarget.value);
|
const value = val.currentTarget.value;
|
||||||
|
stateJPM.create.form.year = value ? Number(value) : 0;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
|
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -14,45 +14,65 @@ function EditDetailDataPengangguran() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState<{
|
||||||
month: stateDetail.update.form.month,
|
month: string;
|
||||||
year: stateDetail.update.form.year,
|
year: number;
|
||||||
totalUnemployment: stateDetail.update.form.totalUnemployment,
|
educatedUnemployment: number;
|
||||||
educatedUnemployment: stateDetail.update.form.educatedUnemployment,
|
uneducatedUnemployment: number;
|
||||||
uneducatedUnemployment: stateDetail.update.form.uneducatedUnemployment,
|
totalUnemployment: number;
|
||||||
percentageChange: stateDetail.update.form.percentageChange || 0, // Ensure it's always a number
|
percentageChange: number | null;
|
||||||
})
|
}>({
|
||||||
|
month: "",
|
||||||
const calculateTotalAndChange = async () => {
|
year: new Date().getFullYear(),
|
||||||
const total = formData.educatedUnemployment + formData.uneducatedUnemployment;
|
educatedUnemployment: 0,
|
||||||
|
uneducatedUnemployment: 0,
|
||||||
// Ambil data bulan sebelumnya
|
totalUnemployment: 0,
|
||||||
const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
|
percentageChange: 0,
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (prev?.totalUnemployment) {
|
|
||||||
percentageChange = Number(
|
|
||||||
(((total - prev.totalUnemployment) / prev.totalUnemployment) * 100).toFixed(1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Update form data and recalculate totals
|
||||||
|
const updateFormData = async (updates: Partial<typeof formData>) => {
|
||||||
|
const newData = { ...formData, ...updates };
|
||||||
|
const { total, percentageChange } = await calculateTotalAndChange();
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...newData,
|
||||||
totalUnemployment: total,
|
totalUnemployment: total,
|
||||||
percentageChange,
|
percentageChange,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTotalAndChange = async () => {
|
||||||
|
const total = formData.educatedUnemployment + formData.uneducatedUnemployment;
|
||||||
|
|
||||||
|
// Calculate percentage change based on previous month's data
|
||||||
|
let percentageChange = 0;
|
||||||
|
const monthOrder = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"];
|
||||||
|
const currentMonthIndex = monthOrder.indexOf(formData.month);
|
||||||
|
|
||||||
|
if (currentMonthIndex !== -1) {
|
||||||
|
let prevMonthIndex = currentMonthIndex - 1;
|
||||||
|
let prevYear = formData.year;
|
||||||
|
|
||||||
|
if (prevMonthIndex < 0) {
|
||||||
|
prevMonthIndex = 11;
|
||||||
|
prevYear--;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevMonth = monthOrder[prevMonthIndex];
|
||||||
|
|
||||||
|
// Get previous month's data
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { total, percentageChange };
|
return { total, percentageChange };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,25 +80,28 @@ function EditDetailDataPengangguran() {
|
|||||||
const loadDetail = async () => {
|
const loadDetail = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await stateDetail.findUnique.load(id); // ambil by ID
|
await stateDetail.findUnique.load(id); // ambil by ID
|
||||||
const data = stateDetail.findUnique.data;
|
const data = stateDetail.findUnique.data;
|
||||||
|
|
||||||
if (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
|
// Set the ID for update
|
||||||
stateDetail.update.id = id;
|
stateDetail.update.id = id;
|
||||||
|
|
||||||
// Update Valtio state with converted year
|
// Update Valtio state with converted year
|
||||||
stateDetail.update.form = {
|
stateDetail.update.form = {
|
||||||
...data,
|
...data,
|
||||||
year: yearValue,
|
year: yearValue,
|
||||||
percentageChange: data.percentageChange || 0 // Ensure it's always a number
|
percentageChange: data.percentageChange || 0 // Ensure it's always a number
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update local formData with converted year
|
// Update local formData with converted year
|
||||||
setFormData({
|
setFormData({
|
||||||
month: data.month,
|
month: data.month,
|
||||||
@@ -94,10 +117,9 @@ function EditDetailDataPengangguran() {
|
|||||||
toast.error("Gagal memuat data detail");
|
toast.error("Gagal memuat data detail");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadDetail();
|
loadDetail();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const { total, percentageChange } = await calculateTotalAndChange();
|
const { total, percentageChange } = await calculateTotalAndChange();
|
||||||
@@ -107,11 +129,11 @@ function EditDetailDataPengangguran() {
|
|||||||
totalUnemployment: total,
|
totalUnemployment: total,
|
||||||
percentageChange,
|
percentageChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const success = await stateDetail.update.submit();
|
const success = await stateDetail.update.submit();
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Detail data pengangguran berhasil diperbarui!");
|
toast.success("Detail data pengangguran berhasil diperbarui!");
|
||||||
router.push("/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran");
|
router.push("/admin/ekonomi/jumlah-pengangguran");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating:", error);
|
console.error("Error updating:", error);
|
||||||
@@ -130,41 +152,38 @@ function EditDetailDataPengangguran() {
|
|||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||||
<Title order={4}>Edit Detail Data Pengangguran</Title>
|
<Title order={4}>Edit Detail Data Pengangguran</Title>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<TextInput
|
<Select
|
||||||
label="Bulan"
|
label="Bulan"
|
||||||
|
data={["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"]}
|
||||||
value={formData.month}
|
value={formData.month}
|
||||||
placeholder="Contoh: Jan, Feb, Mar"
|
onChange={async (val) => {
|
||||||
onChange={(val) => (setFormData({
|
await updateFormData({ month: val || "" });
|
||||||
...formData,
|
}}
|
||||||
month: val.currentTarget.value
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
label="Tahun"
|
label="Tahun"
|
||||||
type="number"
|
|
||||||
value={formData.year}
|
value={formData.year}
|
||||||
onChange={(val) => (setFormData({
|
onChange={async (val) => {
|
||||||
...formData,
|
await updateFormData({ year: Number(val) });
|
||||||
year: Number(val.currentTarget.value)
|
}}
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Pengangguran Terdidik"
|
label="Pengangguran Terdidik"
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.educatedUnemployment}
|
value={formData.educatedUnemployment}
|
||||||
onChange={(val) => (setFormData({
|
onChange={async (val) => {
|
||||||
...formData,
|
const value = Number(val.currentTarget.value) || 0;
|
||||||
educatedUnemployment: Number(val.currentTarget.value)
|
await updateFormData({ educatedUnemployment: value });
|
||||||
}))}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Pengangguran Tidak Terdidik"
|
label="Pengangguran Tidak Terdidik"
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.uneducatedUnemployment}
|
value={formData.uneducatedUnemployment}
|
||||||
onChange={(val) => (setFormData({
|
onChange={async (val) => {
|
||||||
...formData,
|
const value = Number(val.currentTarget.value) || 0;
|
||||||
uneducatedUnemployment: Number(val.currentTarget.value)
|
await updateFormData({ uneducatedUnemployment: value });
|
||||||
}))}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text fz="sm" fw={500}>
|
<Text fz="sm" fw={500}>
|
||||||
Total Otomatis: {formData.totalUnemployment}
|
Total Otomatis: {formData.totalUnemployment}
|
||||||
|
|||||||
@@ -59,11 +59,16 @@ function DetailJumlahPengangguran() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"}>Perubahan</Text>
|
<Text fw={"bold"}>Perubahan</Text>
|
||||||
<Text>{stateDetail.findUnique.data?.percentageChange}</Text>
|
<Text>
|
||||||
|
{stateDetail.findUnique.data?.percentageChange !== null &&
|
||||||
|
stateDetail.findUnique.data?.percentageChange !== undefined
|
||||||
|
? `${stateDetail.findUnique.data.percentageChange}%`
|
||||||
|
: 'Tidak ada data perubahan'}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"}>Tahun</Text>
|
<Text fw={"bold"}>Tahun</Text>
|
||||||
<Text>{stateDetail.findUnique.data?.year ? new Date(stateDetail.findUnique.data.year).getFullYear() : ''}</Text>
|
<Text>{stateDetail.findUnique.data?.year || ''}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"}>Bulan</Text>
|
<Text fw={"bold"}>Bulan</Text>
|
||||||
@@ -76,14 +81,14 @@ function DetailJumlahPengangguran() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Flex gap={"xs"}>
|
<Flex gap={"xs"}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (stateDetail.findUnique.data) {
|
if (stateDetail.findUnique.data) {
|
||||||
setSelectedId(stateDetail.findUnique.data.id);
|
setSelectedId(stateDetail.findUnique.data.id);
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
|
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
|
||||||
color={"red"}>
|
color={"red"}>
|
||||||
<IconX size={20} />
|
<IconX size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green">
|
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
|
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -14,10 +14,15 @@ function CreateJumlahPengangguran() {
|
|||||||
const [chartData, setChartData] = useState<any[]>([]);
|
const [chartData, setChartData] = useState<any[]>([]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const monthOptions = [
|
||||||
|
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
|
||||||
|
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
|
||||||
|
];
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateDetail.create.form = {
|
stateDetail.create.form = {
|
||||||
month: "",
|
month: monthOptions[new Date().getMonth()], // Default to current month
|
||||||
year: 0,
|
year: new Date().getFullYear(), // Default to current year
|
||||||
totalUnemployment: 0,
|
totalUnemployment: 0,
|
||||||
educatedUnemployment: 0,
|
educatedUnemployment: 0,
|
||||||
uneducatedUnemployment: 0,
|
uneducatedUnemployment: 0,
|
||||||
@@ -68,7 +73,7 @@ function CreateJumlahPengangguran() {
|
|||||||
setChartData([stateDetail.findUnique.data]);
|
setChartData([stateDetail.findUnique.data]);
|
||||||
}
|
}
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push('/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran');
|
router.push('/admin/ekonomi/jumlah-pengangguran');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,58 +81,66 @@ function CreateJumlahPengangguran() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Box mb={10}>
|
<Box mb={10}>
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25}/>
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
|
||||||
<Title order={4}>Tambah Detail Data Pengangguran</Title>
|
<Title order={4}>Tambah Data Pengangguran</Title>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs" mt="md">
|
||||||
<TextInput
|
<Select
|
||||||
label="Bulan"
|
label="Bulan"
|
||||||
|
placeholder="Pilih bulan"
|
||||||
|
data={monthOptions}
|
||||||
value={stateDetail.create.form.month}
|
value={stateDetail.create.form.month}
|
||||||
placeholder="Contoh: Jan, Feb, Mar"
|
onChange={(value) => {
|
||||||
onChange={(e) => (stateDetail.create.form.month = e.currentTarget.value)}
|
stateDetail.create.form.month = value || '';
|
||||||
|
calculateTotalAndChange();
|
||||||
|
}}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
label="Tahun"
|
label="Tahun"
|
||||||
type="date"
|
|
||||||
value={stateDetail.create.form.year}
|
value={stateDetail.create.form.year}
|
||||||
onChange={(e) =>
|
onChange={(value) => {
|
||||||
(stateDetail.create.form.year = Number(e.currentTarget.value))
|
stateDetail.create.form.year = Number(value) || new Date().getFullYear();
|
||||||
}
|
calculateTotalAndChange();
|
||||||
|
}}
|
||||||
|
min={2000}
|
||||||
|
max={2100}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
label="Pengangguran Terdidik"
|
label="Pengangguran Terdidik"
|
||||||
type="number"
|
|
||||||
value={stateDetail.create.form.educatedUnemployment}
|
value={stateDetail.create.form.educatedUnemployment}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
stateDetail.create.form.educatedUnemployment = Number(
|
stateDetail.create.form.educatedUnemployment = Number(value) || 0;
|
||||||
e.currentTarget.value,
|
calculateTotalAndChange();
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
|
min={0}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
label="Pengangguran Tidak Terdidik"
|
label="Pengangguran Tidak Terdidik"
|
||||||
type="number"
|
|
||||||
value={stateDetail.create.form.uneducatedUnemployment}
|
value={stateDetail.create.form.uneducatedUnemployment}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
stateDetail.create.form.uneducatedUnemployment = Number(
|
stateDetail.create.form.uneducatedUnemployment = Number(value) || 0;
|
||||||
e.currentTarget.value,
|
calculateTotalAndChange();
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
|
min={0}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<Text fz="sm" fw={500}>
|
<Text fz="sm" fw={500}>
|
||||||
Total Otomatis: {stateDetail.create.form.totalUnemployment}
|
Total Otomatis: {stateDetail.create.form.totalUnemployment.toLocaleString()}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="sm" fw={500}>
|
<Text fz="sm" fw={500}>
|
||||||
Perubahan Otomatis:{" "}
|
Perubahan Otomatis: {stateDetail.create.form.percentageChange.toFixed(1)}%
|
||||||
{stateDetail.create.form.percentageChange !== null
|
|
||||||
? `${stateDetail.create.form.percentageChange}%`
|
|
||||||
: '-'}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Group>
|
<Group mt="md">
|
||||||
<Button bg={colors['blue-button']} mt={10} onClick={handleSubmit}>
|
<Button
|
||||||
Submit
|
onClick={handleSubmit}
|
||||||
|
disabled={!stateDetail.create.form.month || !stateDetail.create.form.year}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ function ListDetailDataPengangguran({search}: {search: string}) {
|
|||||||
setChartData(stateDetail.findMany.data.map((item) => ({
|
setChartData(stateDetail.findMany.data.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
month: item.month,
|
month: item.month,
|
||||||
year: item.year instanceof Date ? item.year.getFullYear() : Number(item.year),
|
year: item.year && typeof item.year === 'object' && 'getFullYear' in item.year ? (item.year as Date).getFullYear() : Number(item.year),
|
||||||
educatedUnemployment: Number(item.educatedUnemployment),
|
educatedUnemployment: Number(item.educatedUnemployment),
|
||||||
uneducatedUnemployment: Number(item.uneducatedUnemployment),
|
uneducatedUnemployment: Number(item.uneducatedUnemployment),
|
||||||
percentageChange: Number(item.percentageChange),
|
percentageChange: Number(item.percentageChange),
|
||||||
|
|||||||
@@ -3,16 +3,25 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
|
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
function EditLowonganKerja() {
|
function EditLowonganKerja() {
|
||||||
const lowonganState = useProxy(lowonganKerjaState)
|
const lowonganState = useProxy(lowonganKerjaState);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
@@ -24,7 +33,7 @@ function EditLowonganKerja() {
|
|||||||
gaji: lowonganKerjaState.update.form.gaji,
|
gaji: lowonganKerjaState.update.form.gaji,
|
||||||
deskripsi: lowonganKerjaState.update.form.deskripsi,
|
deskripsi: lowonganKerjaState.update.form.deskripsi,
|
||||||
kualifikasi: lowonganKerjaState.update.form.kualifikasi,
|
kualifikasi: lowonganKerjaState.update.form.kualifikasi,
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadLowongan = async () => {
|
const loadLowongan = async () => {
|
||||||
@@ -32,7 +41,7 @@ function EditLowonganKerja() {
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await lowonganState.update.load(id); // akses langsung, bukan dari proxy
|
const data = await lowonganState.update.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
posisi: data.posisi || '',
|
posisi: data.posisi || '',
|
||||||
@@ -51,20 +60,17 @@ function EditLowonganKerja() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadLowongan();
|
loadLowongan();
|
||||||
}, [params?.id])
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
// Set the ID for the update
|
|
||||||
lowonganState.update.id = params?.id as string;
|
lowonganState.update.id = params?.id as string;
|
||||||
|
|
||||||
// Update the form state
|
|
||||||
lowonganState.update.form = {
|
lowonganState.update.form = {
|
||||||
...lowonganState.update.form,
|
...lowonganState.update.form,
|
||||||
...formData
|
...formData,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call the update function
|
|
||||||
await lowonganState.update.update();
|
await lowonganState.update.update();
|
||||||
toast.success("Lowongan kerja berhasil diperbarui!");
|
toast.success("Lowongan kerja berhasil diperbarui!");
|
||||||
router.push("/admin/ekonomi/lowongan-kerja-lokal");
|
router.push("/admin/ekonomi/lowongan-kerja-lokal");
|
||||||
@@ -72,79 +78,105 @@ function EditLowonganKerja() {
|
|||||||
console.error("Error updating lowongan kerja:", error);
|
console.error("Error updating lowongan kerja:", error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui lowongan kerja");
|
toast.error("Terjadi kesalahan saat memperbarui lowongan kerja");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan tombol back */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<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'}>
|
{/* Card Form */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Edit Lowongan Kerja Lokal</Title>
|
w={{ base: '100%', md: '60%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Posisi"
|
||||||
|
placeholder="Masukkan posisi"
|
||||||
value={formData.posisi}
|
value={formData.posisi}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, posisi: e.target.value })}
|
||||||
setFormData(prev => ({ ...prev, posisi: val.target.value }));
|
required
|
||||||
}}
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>}
|
|
||||||
placeholder='Masukkan posisi'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Perusahaan"
|
||||||
|
placeholder="Masukkan nama perusahaan"
|
||||||
value={formData.namaPerusahaan}
|
value={formData.namaPerusahaan}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, namaPerusahaan: e.target.value })}
|
||||||
setFormData(prev => ({ ...prev, namaPerusahaan: val.target.value }));
|
required
|
||||||
}}
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>}
|
|
||||||
placeholder='Masukkan nama perusahaan'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Lokasi"
|
||||||
|
placeholder="Masukkan lokasi"
|
||||||
value={formData.lokasi}
|
value={formData.lokasi}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
|
||||||
setFormData(prev => ({ ...prev, lokasi: val.target.value }));
|
required
|
||||||
}}
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
|
|
||||||
placeholder='Masukkan lokasi'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Tipe Pekerjaan"
|
||||||
|
placeholder="Masukkan tipe pekerjaan"
|
||||||
value={formData.tipePekerjaan}
|
value={formData.tipePekerjaan}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, tipePekerjaan: e.target.value })}
|
||||||
setFormData(prev => ({ ...prev, tipePekerjaan: val.target.value }));
|
required
|
||||||
}}
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>}
|
|
||||||
placeholder='Masukkan tipe pekerjaan'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Gaji (per bulan)"
|
||||||
|
placeholder="Masukkan gaji"
|
||||||
value={formData.gaji}
|
value={formData.gaji}
|
||||||
onChange={(val) => {
|
onChange={(e) => setFormData({ ...formData, gaji: e.target.value })}
|
||||||
setFormData(prev => ({ ...prev, gaji: val.target.value }));
|
required
|
||||||
}}
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
|
|
||||||
placeholder='Masukkan gaji'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Lowongan Kerja
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
|
||||||
setFormData(prev => ({ ...prev, deskripsi: val }));
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Kualifikasi Lowongan Kerja</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Kualifikasi Lowongan Kerja
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.kualifikasi}
|
value={formData.kualifikasi}
|
||||||
onChange={(val) => {
|
onChange={(val) => setFormData({ ...formData, kualifikasi: val })}
|
||||||
setFormData(prev => ({ ...prev, kualifikasi: val }));
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,128 +1,148 @@
|
|||||||
'use client'
|
'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 { 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 { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
|
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function DetailLowonganKerjaLokal() {
|
function DetailLowonganKerjaLokal() {
|
||||||
const lowonganState = useProxy(lowonganKerjaState)
|
const lowonganState = useProxy(lowonganKerjaState);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
lowonganState.findUnique.load(params?.id as string)
|
lowonganState.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
lowonganState.delete.byId(selectedId)
|
lowonganState.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/ekonomi/lowongan-kerja-lokal")
|
router.push("/admin/ekonomi/lowongan-kerja-lokal");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!lowonganState.findUnique.data) {
|
if (!lowonganState.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={40} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = lowonganState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
<Button
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
variant="subtle"
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
onClick={() => router.back()}
|
||||||
</Button>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
</Box>
|
mb={15}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
>
|
||||||
<Stack>
|
Kembali
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Lowongan Kerja Lokal</Text>
|
</Button>
|
||||||
{lowonganState.findUnique.data ? (
|
|
||||||
<Paper key={lowonganState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
<Paper
|
||||||
<Stack gap={"xs"}>
|
withBorder
|
||||||
<Box>
|
w={{ base: "100%", md: "70%" }}
|
||||||
<Text fw={"bold"} fz={"lg"}>Bekerja Sebagai</Text>
|
bg={colors['white-1']}
|
||||||
<Text fz={"lg"}>{lowonganState.findUnique.data?.posisi}</Text>
|
p="lg"
|
||||||
</Box>
|
radius="md"
|
||||||
<Box>
|
shadow="sm"
|
||||||
<Text fw={"bold"} fz={"lg"}>Nama Usaha</Text>
|
>
|
||||||
<Text fz={"lg"}>{lowonganState.findUnique.data?.namaPerusahaan}</Text>
|
<Stack gap="md">
|
||||||
</Box>
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
<Box>
|
Detail Lowongan Kerja Lokal
|
||||||
<Text fw={"bold"} fz={"lg"}>Lokasi</Text>
|
</Text>
|
||||||
<Text fz={"lg"}>{lowonganState.findUnique.data?.lokasi}</Text>
|
|
||||||
</Box>
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
<Box>
|
<Stack gap="sm">
|
||||||
<Text fw={"bold"} fz={"lg"}>Tipe Pekerjaan</Text>
|
<Box>
|
||||||
<Text fz={"lg"}>{lowonganState.findUnique.data?.tipePekerjaan}</Text>
|
<Text fz="lg" fw="bold">Bekerja Sebagai</Text>
|
||||||
</Box>
|
<Text fz="md" c="dimmed">{data.posisi || '-'}</Text>
|
||||||
<Box>
|
</Box>
|
||||||
<Text fw={"bold"} fz={"lg"}>Gaji</Text>
|
|
||||||
<Text fz={"lg"}>{lowonganState.findUnique.data?.gaji}</Text>
|
<Box>
|
||||||
</Box>
|
<Text fz="lg" fw="bold">Nama Usaha</Text>
|
||||||
<Box>
|
<Text fz="md" c="dimmed">{data.namaPerusahaan || '-'}</Text>
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
</Box>
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: lowonganState.findUnique.data?.deskripsi }} />
|
|
||||||
</Box>
|
<Box>
|
||||||
<Box>
|
<Text fz="lg" fw="bold">Lokasi</Text>
|
||||||
<Text fw={"bold"} fz={"lg"}>Kualifikasi</Text>
|
<Text fz="md" c="dimmed">{data.lokasi || '-'}</Text>
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: lowonganState.findUnique.data?.kualifikasi }} />
|
</Box>
|
||||||
</Box>
|
|
||||||
<Flex gap={"xs"} mt={10}>
|
<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
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (lowonganState.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(lowonganState.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={lowonganState.delete.loading || !lowonganState.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Lowongan" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (lowonganState.findUnique.data) {
|
onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${data.id}/edit`)}
|
||||||
router.push(`/admin/ekonomi/lowongan-kerja-lokal/${lowonganState.findUnique.data.id}/edit`);
|
variant="light"
|
||||||
}
|
radius="md"
|
||||||
}}
|
size="md"
|
||||||
disabled={!lowonganState.findUnique.data}
|
|
||||||
color={"green"}
|
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus lowongan kerja ini?'
|
text="Apakah Anda yakin ingin menghapus lowongan kerja ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DetailLowonganKerjaLokal;
|
export default DetailLowonganKerjaLokal;
|
||||||
|
|||||||
@@ -1,88 +1,123 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import CreateEditor from '../../../_com/createEditor';
|
import CreateEditor from '../../../_com/createEditor';
|
||||||
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
|
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
|
||||||
|
|
||||||
|
|
||||||
function CreateLowonganKerja() {
|
function CreateLowonganKerja() {
|
||||||
const lowonganState = useProxy(lowonganKerjaState)
|
const lowonganState = useProxy(lowonganKerjaState);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
lowonganState.create.form = {
|
lowonganState.create.form = {
|
||||||
posisi: "",
|
posisi: '',
|
||||||
namaPerusahaan: "",
|
namaPerusahaan: '',
|
||||||
lokasi: "",
|
lokasi: '',
|
||||||
tipePekerjaan: "",
|
tipePekerjaan: '',
|
||||||
gaji: "",
|
gaji: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
kualifikasi: "",
|
kualifikasi: '',
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await lowonganState.create.create()
|
await lowonganState.create.create();
|
||||||
resetForm()
|
resetForm();
|
||||||
router.push("/admin/ekonomi/lowongan-kerja-lokal")
|
router.push('/admin/ekonomi/lowongan-kerja-lokal');
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan tombol kembali */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25}/>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
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'}>
|
{/* Card Form */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Lowongan Kerja Lokal</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={lowonganState.create.form.posisi}
|
value={lowonganState.create.form.posisi}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
lowonganState.create.form.posisi = val.target.value;
|
(lowonganState.create.form.posisi = val.target.value)
|
||||||
}}
|
}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>}
|
label="Posisi"
|
||||||
placeholder='Masukkan posisi'
|
placeholder="Masukkan posisi"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={lowonganState.create.form.namaPerusahaan}
|
value={lowonganState.create.form.namaPerusahaan}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
lowonganState.create.form.namaPerusahaan = val.target.value;
|
(lowonganState.create.form.namaPerusahaan = val.target.value)
|
||||||
}}
|
}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>}
|
label="Nama Perusahaan"
|
||||||
placeholder='Masukkan nama perusahaan'
|
placeholder="Masukkan nama perusahaan"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={lowonganState.create.form.lokasi}
|
value={lowonganState.create.form.lokasi}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
lowonganState.create.form.lokasi = val.target.value;
|
(lowonganState.create.form.lokasi = val.target.value)
|
||||||
}}
|
}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
|
label="Lokasi"
|
||||||
placeholder='Masukkan lokasi'
|
placeholder="Masukkan lokasi"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={lowonganState.create.form.tipePekerjaan}
|
value={lowonganState.create.form.tipePekerjaan}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
lowonganState.create.form.tipePekerjaan = val.target.value;
|
(lowonganState.create.form.tipePekerjaan = val.target.value)
|
||||||
}}
|
}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>}
|
label="Tipe Pekerjaan"
|
||||||
placeholder='Masukkan tipe pekerjaan'
|
placeholder="Masukkan tipe pekerjaan"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={lowonganState.create.form.gaji}
|
value={lowonganState.create.form.gaji}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
lowonganState.create.form.gaji = val.target.value;
|
(lowonganState.create.form.gaji = val.target.value)
|
||||||
}}
|
}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
|
label="Gaji (per bulan)"
|
||||||
placeholder='Masukkan gaji'
|
placeholder="Masukkan gaji"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Lowongan Kerja
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={lowonganState.create.form.deskripsi}
|
value={lowonganState.create.form.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
@@ -90,8 +125,11 @@ function CreateLowonganKerja() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Kualifikasi Lowongan Kerja</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Kualifikasi Lowongan Kerja
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={lowonganState.create.form.kualifikasi}
|
value={lowonganState.create.form.kualifikasi}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
@@ -99,12 +137,25 @@ function CreateLowonganKerja() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,40 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
Box,
|
||||||
import HeaderSearch from '../../_com/header';
|
Button,
|
||||||
import JudulList from '../../_com/judulList';
|
Center,
|
||||||
import { useRouter } from 'next/navigation';
|
Group,
|
||||||
import lowonganKerjaState from '../../_state/ekonomi/lowongan-kerja';
|
Pagination,
|
||||||
import { useProxy } from 'valtio/utils';
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../_com/header';
|
||||||
|
import lowonganKerjaState from '../../_state/ekonomi/lowongan-kerja';
|
||||||
|
|
||||||
function LowonganKerjaLokal() {
|
function LowonganKerjaLokal() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Lowongan Kerja Lokal'
|
title="Lowongan Kerja Lokal"
|
||||||
placeholder='pencarian'
|
placeholder="Cari pekerjaan atau perusahaan..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -27,74 +45,117 @@ function LowonganKerjaLokal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListLowonganKerjaLokal({ search }: { search: string }) {
|
function ListLowonganKerjaLokal({ search }: { search: string }) {
|
||||||
const lowonganState = useProxy(lowonganKerjaState)
|
const stateLowongan = useProxy(lowonganKerjaState);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const { data, page, totalPages, loading, load } = stateLowongan.findMany;
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = lowonganState.findMany
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, search);
|
||||||
}, [page, search])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || [];
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Lowongan Kerja Lokal'
|
<Title order={4}>Daftar Lowongan Kerja Lokal</Title>
|
||||||
href='/admin/ekonomi/lowongan-kerja-lokal/create'
|
<Tooltip label="Tambah Lowongan Kerja" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Pekerjaan</TableTh>
|
onClick={() =>
|
||||||
<TableTh>Nama Perusahaan</TableTh>
|
router.push('/admin/ekonomi/lowongan-kerja-lokal/create')
|
||||||
<TableTh>Lokasi</TableTh>
|
}
|
||||||
<TableTh>Detail</TableTh>
|
>
|
||||||
|
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>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.length > 0 ? (
|
||||||
<TableTr key={item.id}>
|
filteredData.map((item) => (
|
||||||
<TableTd>
|
<TableTr key={item.id}>
|
||||||
<Text fz={"md"}>{item.posisi}</Text>
|
<TableTd style={{ width: '25%' }}>
|
||||||
</TableTd>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
<TableTd>
|
{item.posisi}
|
||||||
<Text fz={"md"}>{item.namaPerusahaan}</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd style={{ width: '25%' }}>
|
||||||
<Text fz={"md"}>{item.lokasi}</Text>
|
<Text truncate fz="sm" c="dimmed">
|
||||||
</TableTd>
|
{item.namaPerusahaan}
|
||||||
<TableTd>
|
</Text>
|
||||||
<Button onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`)}>
|
</TableTd>
|
||||||
<IconDeviceImac size={20} />
|
<TableTd style={{ width: '20%' }}>
|
||||||
</Button>
|
<Text truncate fz="sm" c="dimmed">
|
||||||
</TableTd>
|
{item.lokasi}
|
||||||
</TableTr>
|
</Text>
|
||||||
))}
|
</TableTd>
|
||||||
</TableTbody>
|
<TableTd style={{ width: '15%' }}>
|
||||||
</Table>
|
<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>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)}
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
my="md"
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,62 +1,131 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { IconShoppingBag, IconCategory } from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const pathname = usePathname()
|
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 handleTabChange = (value: string | null) => {
|
const tabs = [
|
||||||
const tab = tabs.find(t => t.value === value)
|
{
|
||||||
if (tab) {
|
label: "Produk Pasar Desa",
|
||||||
router.push(tab.href)
|
value: "produkpasardesa",
|
||||||
}
|
href: "/admin/ekonomi/pasar-desa/produk-pasar-desa",
|
||||||
setActiveTab(value)
|
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(() => {
|
useEffect(() => {
|
||||||
const match = tabs.find(tab => tab.href === pathname)
|
const match = tabs.find((tab) => tab.href === pathname);
|
||||||
if (match) {
|
if (match) {
|
||||||
setActiveTab(match.value)
|
setActiveTab(match.value);
|
||||||
}
|
}
|
||||||
}, [pathname])
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Pasar Desa</Title>
|
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
Pasar Desa
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
</Title>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
<Tabs
|
||||||
))}
|
color={colors['blue-button']}
|
||||||
</TabsList>
|
variant="pills"
|
||||||
{tabs.map((e, i) => (
|
value={activeTab}
|
||||||
<TabsPanel key={i} value={e.value}>
|
onChange={handleTabChange}
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
radius="lg"
|
||||||
<></>
|
keepMounted={false}
|
||||||
</TabsPanel>
|
>
|
||||||
))}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
</Tabs>
|
<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}
|
{children}
|
||||||
</Stack>
|
</TabsPanel>
|
||||||
);
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabs;
|
export default LayoutTabs;
|
||||||
|
|||||||
@@ -5,7 +5,17 @@ import { useParams, useRouter } from 'next/navigation';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
|
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -16,7 +26,7 @@ function EditKategoriProduk() {
|
|||||||
const statePasar = useProxy(pasarDesaState.kategoriProduk);
|
const statePasar = useProxy(pasarDesaState.kategoriProduk);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
nama: "",
|
nama: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,8 +44,8 @@ function EditKategoriProduk() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading kategori produk:", error);
|
console.error('Error loading kategori produk:', error);
|
||||||
toast.error("Gagal memuat data kategori produk");
|
toast.error('Gagal memuat data kategori produk');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,7 +63,6 @@ function EditKategoriProduk() {
|
|||||||
nama: formData.nama.trim(),
|
nama: formData.nama.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Safety check tambahan: pastikan ID tidak kosong
|
|
||||||
if (!statePasar.edit.id) {
|
if (!statePasar.edit.id) {
|
||||||
statePasar.edit.id = id; // fallback
|
statePasar.edit.id = id; // fallback
|
||||||
}
|
}
|
||||||
@@ -61,33 +70,60 @@ function EditKategoriProduk() {
|
|||||||
const success = await statePasar.edit.update();
|
const success = await statePasar.edit.update();
|
||||||
|
|
||||||
if (success) {
|
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) {
|
} catch (error) {
|
||||||
console.error("Error updating kategori produk:", error);
|
console.error('Error updating kategori produk:', error);
|
||||||
// toast akan ditampilkan dari fungsi update
|
toast.error('Terjadi kesalahan saat memperbarui kategori produk');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan tombol back */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<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'}>
|
{/* Card form */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Edit Kategori Produk</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label={<Text fw="bold" fz="sm">Nama Kategori Produk</Text>}
|
||||||
|
placeholder="Masukkan nama kategori produk"
|
||||||
value={formData.nama}
|
value={formData.nama}
|
||||||
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Produk</Text>}
|
required
|
||||||
placeholder='Masukkan nama kategori produk'
|
|
||||||
/>
|
/>
|
||||||
<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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
import React, { useEffect } from 'react';
|
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 { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
|
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() {
|
function CreateKategoriProduk() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const statePasar = useProxy(pasarDesaState.kategoriProduk)
|
const statePasar = useProxy(pasarDesaState.kategoriProduk);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
statePasar.findMany.load();
|
statePasar.findMany.load();
|
||||||
@@ -18,42 +28,73 @@ function CreateKategoriProduk() {
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
statePasar.create.form = {
|
statePasar.create.form = {
|
||||||
nama: "",
|
nama: '',
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
if (!statePasar.create.form.nama) {
|
||||||
|
return toast.warn('Nama kategori produk wajib diisi');
|
||||||
|
}
|
||||||
|
|
||||||
await statePasar.create.create();
|
await statePasar.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/ekonomi/pasar-desa/kategori-produk")
|
router.push('/admin/ekonomi/pasar-desa/kategori-produk');
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box>
|
{/* Header dengan tombol kembali */}
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</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'}>
|
{/* Card form */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Kategori Produk</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
<TextInput
|
bg={colors['white-1']}
|
||||||
value={statePasar.create.form.nama}
|
p="lg"
|
||||||
onChange={(val) => {
|
radius="md"
|
||||||
statePasar.create.form.nama = val.target.value;
|
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'
|
Simpan
|
||||||
/>
|
</Button>
|
||||||
<Group>
|
</Group>
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
</Stack>
|
||||||
</Group>
|
</Paper>
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,36 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { 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 { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
|
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
|
||||||
|
|
||||||
|
|
||||||
function PasarDesa() {
|
function KategoriProduk() {
|
||||||
const [search2, setSearch2] = useState("")
|
const [search2, setSearch2] = useState("")
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Kategori Produk'
|
title='Kategori Produk'
|
||||||
placeholder='pencarian'
|
placeholder='Cari nama kategori produk...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search2}
|
value={search2}
|
||||||
onChange={(e) => setSearch2(e.currentTarget.value)}
|
onChange={(e) => setSearch2(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<ListPasarDesa search2={search2} />
|
<ListKategoriProduk search2={search2} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListPasarDesa({ search2 }: { search2: string }) {
|
function ListKategoriProduk({ search2 }: { search2: string }) {
|
||||||
const statePasar = useProxy(pasarDesaState.kategoriProduk)
|
const statePasar = useProxy(pasarDesaState.kategoriProduk)
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
// const params = useParams()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -60,56 +58,102 @@ function ListPasarDesa({ search2 }: { search2: string }) {
|
|||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Produk Pasar Desa'
|
<Title order={4}>Daftar Kategori Produk</Title>
|
||||||
href='/admin/ekonomi/pasar-desa/kategori-produk/create'
|
<Tooltip label="Tambah Kategori Produk" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Nama Kategori</TableTh>
|
onClick={() => router.push('/admin/ekonomi/pasar-desa/kategori-produk/create')}
|
||||||
<TableTh>Edit</TableTh>
|
>
|
||||||
<TableTh>Delete</TableTh>
|
Tambah Baru
|
||||||
</TableTr>
|
</Button>
|
||||||
</TableThead>
|
</Tooltip>
|
||||||
<TableTbody>
|
</Group>
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<TableTd>{item.nama}</TableTd>
|
<Table highlightOnHover>
|
||||||
<TableTd>
|
<TableThead>
|
||||||
<Button color="green" onClick={() => router.push(`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`)}>
|
<TableTr>
|
||||||
<IconEdit size={20} />
|
<TableTh style={{ width: '60%' }}>Nama Kategori</TableTh>
|
||||||
</Button>
|
<TableTh style={{ width: '20%' }}>Edit</TableTh>
|
||||||
</TableTd>
|
<TableTh style={{ width: '20%' }}>Delete</TableTh>
|
||||||
<TableTd>
|
|
||||||
<Button color="red" onClick={() => {
|
|
||||||
setSelectedId(item.id)
|
|
||||||
setModalHapus(true)
|
|
||||||
}}>
|
|
||||||
<IconX size={20} />
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
</TableTbody>
|
<TableTbody>
|
||||||
</Table>
|
{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>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)}
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10)
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
my={"md"}
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
{/* Modal Konfirmasi Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
@@ -118,7 +162,7 @@ function ListPasarDesa({ search2 }: { search2: string }) {
|
|||||||
text='Apakah anda yakin ingin menghapus kategori produk ini?'
|
text='Apakah anda yakin ingin menghapus kategori produk ini?'
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PasarDesa;
|
export default KategoriProduk;
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
|
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
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 { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
@@ -13,20 +25,20 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function EditPasarDesa() {
|
function EditPasarDesa() {
|
||||||
const pasarState = useProxy(pasarDesaState)
|
const pasarState = useProxy(pasarDesaState);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
nama: pasarState.pasarDesa.edit.form.nama || "",
|
nama: pasarState.pasarDesa.edit.form.nama || '',
|
||||||
harga: pasarState.pasarDesa.edit.form.harga || 0,
|
harga: pasarState.pasarDesa.edit.form.harga || 0,
|
||||||
alamatUsaha: pasarState.pasarDesa.edit.form.alamatUsaha || "",
|
alamatUsaha: pasarState.pasarDesa.edit.form.alamatUsaha || '',
|
||||||
imageId: pasarState.pasarDesa.edit.form.imageId || "",
|
imageId: pasarState.pasarDesa.edit.form.imageId || '',
|
||||||
rating: pasarState.pasarDesa.edit.form.rating || 0,
|
rating: pasarState.pasarDesa.edit.form.rating || 0,
|
||||||
kategoriId: pasarState.pasarDesa.edit.form.kategoriId || [],
|
kategoriId: pasarState.pasarDesa.edit.form.kategoriId || [],
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pasarState.kategoriProduk.findMany.load();
|
pasarState.kategoriProduk.findMany.load();
|
||||||
@@ -38,184 +50,192 @@ function EditPasarDesa() {
|
|||||||
const data = await pasarState.pasarDesa.edit.load(id);
|
const data = await pasarState.pasarDesa.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
nama: data.nama || "",
|
nama: data.nama || '',
|
||||||
harga: data.harga || 0,
|
harga: data.harga || 0,
|
||||||
alamatUsaha: data.alamatUsaha || "",
|
alamatUsaha: data.alamatUsaha || '',
|
||||||
imageId: data.imageId || "",
|
imageId: data.imageId || '',
|
||||||
rating: data.rating || 0,
|
rating: data.rating || 0,
|
||||||
// Use the IDs from KategoriToPasar relationship
|
|
||||||
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
|
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) {
|
} catch (error) {
|
||||||
console.error("Error loading pasar desa:", error);
|
console.error('Error loading pasar desa:', error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error ? error.message : "Gagal mengambil data pasar desa"
|
error instanceof Error ? error.message : 'Gagal mengambil data pasar desa'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
loadPasarDesa();
|
loadPasarDesa();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
pasarState.pasarDesa.edit.form = {
|
pasarState.pasarDesa.edit.form = { ...pasarState.pasarDesa.edit.form, ...formData };
|
||||||
...pasarState.pasarDesa.edit.form,
|
|
||||||
nama: formData.nama,
|
|
||||||
harga: formData.harga,
|
|
||||||
alamatUsaha: formData.alamatUsaha,
|
|
||||||
imageId: formData.imageId,
|
|
||||||
rating: formData.rating,
|
|
||||||
kategoriId: formData.kategoriId,
|
|
||||||
}
|
|
||||||
if (file) {
|
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;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update imageId in global state
|
|
||||||
pasarState.pasarDesa.edit.form.imageId = uploaded.id;
|
pasarState.pasarDesa.edit.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
await pasarState.pasarDesa.edit.update();
|
await pasarState.pasarDesa.edit.update();
|
||||||
toast.success("Tips Keamanan berhasil diperbarui!");
|
toast.success('Pasar desa berhasil diperbarui!');
|
||||||
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa");
|
router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating pasar desa:", error);
|
console.error('Error updating pasar desa:', error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui pasar desa");
|
toast.error('Terjadi kesalahan saat memperbarui pasar desa');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Button>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Box>
|
</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'}>
|
<Paper
|
||||||
<Stack gap={"xs"}>
|
w={{ base: '100%', md: '50%' }}
|
||||||
<Title order={4}>Edit Pasar Desa</Title>
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Produk
|
||||||
<Dropzone
|
</Text>
|
||||||
onDrop={(files) => {
|
<Dropzone
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
onDrop={(files) => {
|
||||||
if (selectedFile) {
|
const selectedFile = files[0];
|
||||||
setFile(selectedFile);
|
if (selectedFile) {
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setFile(selectedFile);
|
||||||
}
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}}
|
}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
}}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
accept={{ 'image/*': [] }}
|
maxSize={5 * 1024 ** 2}
|
||||||
>
|
accept={{ 'image/*': [] }}
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
radius="md"
|
||||||
<Dropzone.Accept>
|
p="xl"
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
>
|
||||||
</Dropzone.Accept>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Accept>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Reject>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</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>
|
{previewImage && (
|
||||||
<Text size="xl" inline>
|
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
<Image
|
||||||
</Text>
|
src={previewImage}
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
alt="Preview Gambar"
|
||||||
Maksimal 5MB dan harus format gambar
|
radius="md"
|
||||||
</Text>
|
style={{
|
||||||
</div>
|
maxHeight: 220,
|
||||||
</Group>
|
objectFit: 'contain',
|
||||||
</Dropzone>
|
border: `1px solid ${colors['blue-button']}`,
|
||||||
|
}}
|
||||||
{/* Tampilkan preview kalau ada */}
|
/>
|
||||||
{previewImage && (
|
</Box>
|
||||||
<Box mt="sm">
|
)}
|
||||||
<Image
|
|
||||||
src={previewImage}
|
|
||||||
alt="Preview"
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Produk"
|
||||||
|
placeholder="Masukkan nama produk"
|
||||||
value={formData.nama}
|
value={formData.nama}
|
||||||
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
|
required
|
||||||
placeholder='Masukkan nama produk'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
label="Harga Produk"
|
||||||
|
placeholder="Masukkan harga produk"
|
||||||
value={formData.harga}
|
value={formData.harga}
|
||||||
onChange={(e) => setFormData({ ...formData, harga: Number(e.target.value) })}
|
onChange={(e) => setFormData({ ...formData, harga: Number(e.target.value) })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
|
required
|
||||||
placeholder='Masukkan harga produk'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
max={5}
|
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}
|
value={formData.rating}
|
||||||
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
|
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
|
required
|
||||||
placeholder='Masukkan rating produk (0-5)'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Alamat Usaha"
|
||||||
|
placeholder="Masukkan alamat usaha"
|
||||||
value={formData.alamatUsaha}
|
value={formData.alamatUsaha}
|
||||||
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
|
required
|
||||||
placeholder='Masukkan alamat usaha'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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
|
<MultiSelect
|
||||||
|
label="Kategori Produk"
|
||||||
|
placeholder="Pilih kategori produk"
|
||||||
value={formData.kategoriId}
|
value={formData.kategoriId}
|
||||||
onChange={(val) => setFormData({ ...formData, kategoriId: val })}
|
onChange={(val) => setFormData({ ...formData, kategoriId: val })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori Produk</Text>}
|
|
||||||
placeholder='Pilih kategori produk'
|
|
||||||
data={
|
data={
|
||||||
pasarState.kategoriProduk.findMany.data?.map((v) => ({
|
pasarState.kategoriProduk.findMany.data?.map((v) => ({
|
||||||
value: v.id, // Make sure this is using the ID
|
value: v.id,
|
||||||
label: v.nama
|
label: v.nama,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
clearable
|
clearable
|
||||||
searchable
|
searchable
|
||||||
required
|
required
|
||||||
error={!formData.kategoriId.length ? "Pilih minimal satu kategori" : undefined}
|
error={!formData.kategoriId.length ? 'Pilih minimal satu kategori' : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group>
|
<Group justify="right">
|
||||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
<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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
|
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core';
|
||||||
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
|
import { IconArrowBack, IconTrash, IconEdit } from '@tabler/icons-react';
|
||||||
import { useRouter, useParams } from 'next/navigation';
|
import { useRouter, useParams } from 'next/navigation';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -10,119 +10,150 @@ import { useShallowEffect } from '@mantine/hooks';
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
|
||||||
function DetailPasarDesa() {
|
function DetailPasarDesa() {
|
||||||
const statePasar = useProxy(pasarDesaState)
|
const statePasar = useProxy(pasarDesaState);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
statePasar.pasarDesa.findUnique.load(params?.id as string)
|
statePasar.pasarDesa.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
statePasar.pasarDesa.delete.byId(selectedId)
|
statePasar.pasarDesa.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa")
|
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!statePasar.pasarDesa.findUnique.data) {
|
if (!statePasar.pasarDesa.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = statePasar.pasarDesa.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
<Button
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
variant="subtle"
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
onClick={() => router.back()}
|
||||||
</Button>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
</Box>
|
mb={15}
|
||||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
|
>
|
||||||
<Stack>
|
Kembali
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Pasar Desa</Text>
|
</Button>
|
||||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
|
withBorder
|
||||||
|
w={{ base: "100%", md: "60%" }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Pasar Desa
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Nama Produk</Text>
|
<Text fz="lg" fw="bold">Nama Produk</Text>
|
||||||
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.nama}</Text>
|
<Text fz="md" c="dimmed">{data.nama || '-'}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Harga Produk</Text>
|
<Text fz="lg" fw="bold">Harga Produk</Text>
|
||||||
<Text fz={"lg"}>Rp.{statePasar.pasarDesa.findUnique.data?.harga}</Text>
|
<Text fz="md" c="dimmed">Rp. {data.harga || '-'}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Rating Produk</Text>
|
<Text fz="lg" fw="bold">Rating Produk</Text>
|
||||||
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.rating}</Text>
|
<Text fz="md" c="dimmed">{data.rating || '-'}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Alamat Usaha</Text>
|
<Text fz="lg" fw="bold">Alamat Usaha</Text>
|
||||||
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.alamatUsaha}</Text>
|
<Text fz="md" c="dimmed">{data.alamatUsaha || '-'}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
<Image src={statePasar.pasarDesa.findUnique.data?.image?.link} alt="gambar" />
|
{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>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
|
<Text fz="lg" fw="bold">Kategori</Text>
|
||||||
<Stack gap={4}>
|
<Stack gap={4}>
|
||||||
{statePasar.pasarDesa.findUnique.data?.KategoriToPasar &&
|
{data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
|
||||||
statePasar.pasarDesa.findUnique.data.KategoriToPasar.length > 0 ? (
|
data.KategoriToPasar.map((kategori) => (
|
||||||
statePasar.pasarDesa.findUnique.data.KategoriToPasar.map((kategori) => (
|
<Text fz="md" c="dimmed" key={kategori.id}>
|
||||||
<Text fz={"lg"} key={kategori.id}>
|
|
||||||
• {kategori.kategori.nama}
|
• {kategori.kategori.nama}
|
||||||
</Text>
|
</Text>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Text fz={"lg"} c="dimmed">
|
<Text fz="sm" c="dimmed">Tidak ada kategori</Text>
|
||||||
Tidak ada kategori
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Group gap="sm">
|
||||||
<Flex gap={"xs"}>
|
<Tooltip label="Hapus Produk" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (statePasar.pasarDesa.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(statePasar.pasarDesa.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={!statePasar.pasarDesa.findUnique.data}
|
variant="light"
|
||||||
color="red">
|
radius="md"
|
||||||
<IconX size={20} />
|
size="md"
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Produk" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (statePasar.pasarDesa.findUnique.data) {
|
onClick={() => router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${data.id}/edit`)}
|
||||||
router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${statePasar.pasarDesa.findUnique.data.id}/edit`);
|
variant="light"
|
||||||
}
|
radius="md"
|
||||||
}}
|
size="md"
|
||||||
disabled={!statePasar.pasarDesa.findUnique.data}
|
>
|
||||||
color="green">
|
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Box>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text="Apakah anda yakin ingin menghapus produk ini?"
|
text="Apakah Anda yakin ingin menghapus produk ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
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 { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -11,9 +23,9 @@ import { toast } from 'react-toastify';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
|
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
|
||||||
|
|
||||||
function CreatePasarDesa() {
|
export default function CreatePasarDesa() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const statePasar = useProxy(pasarDesaState)
|
const statePasar = useProxy(pasarDesaState);
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
@@ -23,170 +35,185 @@ function CreatePasarDesa() {
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
statePasar.pasarDesa.create.form = {
|
statePasar.pasarDesa.create.form = {
|
||||||
nama: "",
|
nama: '',
|
||||||
harga: 0,
|
harga: 0,
|
||||||
alamatUsaha: "",
|
alamatUsaha: '',
|
||||||
imageId: "",
|
imageId: '',
|
||||||
rating: 0,
|
rating: 0,
|
||||||
kategoriId: [],
|
kategoriId: [],
|
||||||
};
|
};
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
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({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
})
|
});
|
||||||
|
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
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;
|
statePasar.pasarDesa.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
await statePasar.pasarDesa.create.create();
|
await statePasar.pasarDesa.create.create();
|
||||||
|
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa")
|
router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header dengan tombol kembali */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<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'}>
|
{/* Card Form */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Pasar Desa</Title>
|
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>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Produk
|
||||||
<Dropzone
|
</Text>
|
||||||
onDrop={(files) => {
|
<Dropzone
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
onDrop={(files) => {
|
||||||
if (selectedFile) {
|
const selectedFile = files[0];
|
||||||
setFile(selectedFile);
|
if (selectedFile) {
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setFile(selectedFile);
|
||||||
}
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}}
|
}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
}}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
accept={{ 'image/*': [] }}
|
maxSize={5 * 1024 ** 2}
|
||||||
>
|
accept={{ 'image/*': [] }}
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
radius="md"
|
||||||
<Dropzone.Accept>
|
p="xl"
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
>
|
||||||
</Dropzone.Accept>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Accept>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Reject>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</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>
|
{previewImage && (
|
||||||
<Text size="xl" inline>
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
<Image
|
||||||
</Text>
|
src={previewImage}
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
alt="Preview Gambar"
|
||||||
Maksimal 5MB dan harus format gambar
|
radius="md"
|
||||||
</Text>
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
</div>
|
/>
|
||||||
</Group>
|
</Box>
|
||||||
</Dropzone>
|
)}
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
|
||||||
{previewImage && (
|
|
||||||
<Box mt="sm">
|
|
||||||
<Image
|
|
||||||
src={previewImage}
|
|
||||||
alt="Preview"
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Nama Produk */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Nama Produk"
|
||||||
|
placeholder="Masukkan nama produk"
|
||||||
value={statePasar.pasarDesa.create.form.nama}
|
value={statePasar.pasarDesa.create.form.nama}
|
||||||
onChange={(val) => {
|
onChange={(e) => (statePasar.pasarDesa.create.form.nama = e.target.value)}
|
||||||
statePasar.pasarDesa.create.form.nama = val.target.value;
|
required
|
||||||
}}
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
|
|
||||||
placeholder='Masukkan nama produk'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Harga Produk */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
label="Harga Produk"
|
||||||
|
placeholder="Masukkan harga produk"
|
||||||
value={statePasar.pasarDesa.create.form.harga}
|
value={statePasar.pasarDesa.create.form.harga}
|
||||||
onChange={(val) => {
|
onChange={(e) => (statePasar.pasarDesa.create.form.harga = Number(e.target.value))}
|
||||||
statePasar.pasarDesa.create.form.harga = Number(val.target.value);
|
required
|
||||||
}}
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
|
|
||||||
placeholder='Masukkan harga produk'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Rating */}
|
||||||
<TextInput
|
<TextInput
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
max={5}
|
max={5}
|
||||||
step={0.1} // bisa pakai 0.1 biar support desimal
|
step={0.1}
|
||||||
|
label="Rating Produk (0–5)"
|
||||||
|
placeholder="Masukkan rating produk"
|
||||||
value={statePasar.pasarDesa.create.form.rating}
|
value={statePasar.pasarDesa.create.form.rating}
|
||||||
onChange={(val) => {
|
onChange={(e) => {
|
||||||
const value = Number(val.target.value);
|
const value = Number(e.target.value);
|
||||||
|
|
||||||
// Validasi manual juga boleh (jaga-jaga)
|
|
||||||
if (value >= 0 && value <= 5) {
|
if (value >= 0 && value <= 5) {
|
||||||
statePasar.pasarDesa.create.form.rating = value;
|
statePasar.pasarDesa.create.form.rating = value;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
|
|
||||||
placeholder='Masukkan rating produk (0-5)'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Alamat Usaha */}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
label="Alamat Usaha"
|
||||||
|
placeholder="Masukkan alamat usaha"
|
||||||
value={statePasar.pasarDesa.create.form.alamatUsaha}
|
value={statePasar.pasarDesa.create.form.alamatUsaha}
|
||||||
onChange={(val) => {
|
onChange={(e) => (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)}
|
||||||
statePasar.pasarDesa.create.form.alamatUsaha = val.target.value;
|
|
||||||
}}
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
|
|
||||||
placeholder='Masukkan alamat usaha'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Kategori Produk */}
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
|
label="Kategori Produk"
|
||||||
|
placeholder="Pilih kategori produk"
|
||||||
value={statePasar.pasarDesa.create.form.kategoriId}
|
value={statePasar.pasarDesa.create.form.kategoriId}
|
||||||
onChange={(val) => {
|
onChange={(val) => (statePasar.pasarDesa.create.form.kategoriId = val)}
|
||||||
statePasar.pasarDesa.create.form.kategoriId = val;
|
|
||||||
}}
|
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kategori Produk</Text>}
|
|
||||||
placeholder='Pilih kategori produk'
|
|
||||||
data={
|
data={
|
||||||
statePasar.kategoriProduk.findMany.data?.map((v) => ({
|
statePasar.kategoriProduk.findMany.data?.map((v) => ({
|
||||||
value: v.id,
|
value: v.id,
|
||||||
label: v.nama
|
label: v.nama,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group>
|
{/* Tombol Submit */}
|
||||||
<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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreatePasarDesa;
|
|
||||||
|
|||||||
@@ -1,22 +1,39 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
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 { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
|
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
function PasarDesa() {
|
function PasarDesa() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Produk Pasar Desa'
|
title="Produk Pasar Desa"
|
||||||
placeholder='pencarian'
|
placeholder="Cari produk pasar desa..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
@@ -27,71 +44,119 @@ function PasarDesa() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListPasarDesa({ search }: { search: string }) {
|
function ListPasarDesa({ search }: { search: string }) {
|
||||||
const statePasar = useProxy(pasarDesaState.pasarDesa)
|
const statePasar = useProxy(pasarDesaState.pasarDesa);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const { data, page, totalPages, loading, load } = statePasar.findMany;
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = statePasar.findMany
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, search);
|
||||||
}, [page, search])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || [];
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Produk Pasar Desa'
|
<Title order={4}>Daftar Produk Pasar Desa</Title>
|
||||||
href='/admin/ekonomi/pasar-desa/produk-pasar-desa/create'
|
<Tooltip label="Tambah Produk Baru" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Nama Produk</TableTh>
|
onClick={() =>
|
||||||
<TableTh>Harga Produk</TableTh>
|
router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa/create')
|
||||||
<TableTh>Rating Produk</TableTh>
|
}
|
||||||
<TableTh>Alamat Usaha</TableTh>
|
>
|
||||||
<TableTh>Detail</TableTh>
|
Tambah Baru
|
||||||
</TableTr>
|
</Button>
|
||||||
</TableThead>
|
</Tooltip>
|
||||||
<TableTbody>
|
</Group>
|
||||||
{filteredData.map((item) => (
|
|
||||||
<TableTr key={item.id}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<TableTd>{item.nama}</TableTd>
|
<Table highlightOnHover>
|
||||||
<TableTd>Rp.{item.harga}</TableTd>
|
<TableThead>
|
||||||
<TableTd>{item.rating}</TableTd>
|
<TableTr>
|
||||||
<TableTd>{item.alamatUsaha}</TableTd>
|
<TableTh style={{ width: '25%' }}>Nama Produk</TableTh>
|
||||||
<TableTd>
|
<TableTh style={{ width: '20%' }}>Harga Produk</TableTh>
|
||||||
<Button onClick={() => router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${item.id}`)}>
|
<TableTh style={{ width: '15%' }}>Rating</TableTh>
|
||||||
<IconDeviceImac size={20} />
|
<TableTh style={{ width: '25%' }}>Alamat Usaha</TableTh>
|
||||||
</Button>
|
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
</TableTbody>
|
<TableTbody>
|
||||||
</Table>
|
{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>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)}
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
my={"md"}
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,67 +1,142 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
IconHierarchy,
|
||||||
|
IconUsers,
|
||||||
|
IconGitBranch,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const pathname = usePathname()
|
const pathname = usePathname();
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "Posisi Organisasi",
|
label: "Posisi Organisasi",
|
||||||
value: "posisiorganisasi",
|
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",
|
label: "Pegawai",
|
||||||
value: "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",
|
label: "Hubungan Organisasi",
|
||||||
value: "hubunganorganisasi",
|
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 handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value)
|
const tab = tabs.find((t) => t.value === value);
|
||||||
if (tab) {
|
if (tab) {
|
||||||
router.push(tab.href)
|
router.push(tab.href);
|
||||||
}
|
}
|
||||||
setActiveTab(value)
|
setActiveTab(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const match = tabs.find(tab => tab.href === pathname)
|
const match = tabs.find((tab) => tab.href === pathname);
|
||||||
if (match) {
|
if (match) {
|
||||||
setActiveTab(match.value)
|
setActiveTab(match.value);
|
||||||
}
|
}
|
||||||
}, [pathname])
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack gap="lg">
|
||||||
<Title order={3}>Struktur Organisasi & SK Pengurus BUMDesa</Title>
|
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
Struktur Organisasi & SK Pengurus BUMDesa
|
||||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
</Title>
|
||||||
{tabs.map((e, i) => (
|
|
||||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
<Tabs
|
||||||
))}
|
color={colors["blue-button"]}
|
||||||
</TabsList>
|
variant="pills"
|
||||||
{tabs.map((e, i) => (
|
value={activeTab}
|
||||||
<TabsPanel key={i} value={e.value}>
|
onChange={handleTabChange}
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
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>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{children}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabs;
|
export default LayoutTabs;
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export default function EditPegawai() {
|
|||||||
const success = await stateOrganisasi.edit.submit();
|
const success = await stateOrganisasi.edit.submit();
|
||||||
|
|
||||||
if (success) {
|
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) {
|
} catch (error) {
|
||||||
console.error("Error updating pegawai:", error);
|
console.error("Error updating pegawai:", error);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function DetailPegawai() {
|
|||||||
statePegawai.delete.byId(selectedId)
|
statePegawai.delete.byId(selectedId)
|
||||||
setModalHapus(false)
|
setModalHapus(false)
|
||||||
setSelectedId(null)
|
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
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (statePegawai.findUnique.data) {
|
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}
|
disabled={!statePegawai.findUnique.data}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function CreatePegawai() {
|
|||||||
// Reset form dan redirect
|
// Reset form dan redirect
|
||||||
resetForm();
|
resetForm();
|
||||||
toast.success("Data pegawai berhasil ditambahkan");
|
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) {
|
} catch (error) {
|
||||||
console.error("Error creating pegawai:", error);
|
console.error("Error creating pegawai:", error);
|
||||||
toast.error("Terjadi kesalahan saat menambahkan pegawai");
|
toast.error("Terjadi kesalahan saat menambahkan pegawai");
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function ListPegawai({ search }: { search: string }) {
|
|||||||
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
|
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
|
||||||
<JudulList
|
<JudulList
|
||||||
title='List Pegawai'
|
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" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table striped withTableBorder withRowBorders>
|
<Table striped withTableBorder withRowBorders>
|
||||||
@@ -131,7 +131,7 @@ function ListPegawai({ search }: { search: string }) {
|
|||||||
</Group>
|
</Group>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<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} />
|
<IconDeviceImacCog size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
|||||||
@@ -3,7 +3,17 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
|
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -17,8 +27,8 @@ function EditPosisiOrganisasi() {
|
|||||||
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
|
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
nama: "",
|
nama: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
hierarki: 0,
|
hierarki: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,7 +40,6 @@ function EditPosisiOrganisasi() {
|
|||||||
const data = await stateOrganisasi.edit.load(id);
|
const data = await stateOrganisasi.edit.load(id);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// pastikan id-nya masuk ke state edit
|
|
||||||
stateOrganisasi.edit.id = id;
|
stateOrganisasi.edit.id = id;
|
||||||
setFormData({
|
setFormData({
|
||||||
nama: data.nama || '',
|
nama: data.nama || '',
|
||||||
@@ -39,8 +48,8 @@ function EditPosisiOrganisasi() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading posisi organisasi:", error);
|
console.error('Error loading posisi organisasi:', error);
|
||||||
toast.error("Gagal memuat data posisi organisasi");
|
toast.error('Gagal memuat data posisi organisasi');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,7 +69,6 @@ function EditPosisiOrganisasi() {
|
|||||||
hierarki: formData.hierarki,
|
hierarki: formData.hierarki,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Safety check tambahan: pastikan ID tidak kosong
|
|
||||||
if (!stateOrganisasi.edit.id) {
|
if (!stateOrganisasi.edit.id) {
|
||||||
stateOrganisasi.edit.id = id; // fallback
|
stateOrganisasi.edit.id = id; // fallback
|
||||||
}
|
}
|
||||||
@@ -68,45 +76,92 @@ function EditPosisiOrganisasi() {
|
|||||||
const success = await stateOrganisasi.edit.update();
|
const success = await stateOrganisasi.edit.update();
|
||||||
|
|
||||||
if (success) {
|
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) {
|
} catch (error) {
|
||||||
console.error("Error updating posisi organisasi:", error);
|
console.error('Error updating posisi organisasi:', error);
|
||||||
// toast akan ditampilkan dari fungsi update
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
<Group mb="md">
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Button
|
||||||
</Button>
|
variant="subtle"
|
||||||
</Box>
|
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'}>
|
<Paper
|
||||||
<Stack gap={"xs"}>
|
w={{ base: '100%', md: '50%' }}
|
||||||
<Title order={4}>Edit Posisi Organisasi</Title>
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={formData.nama}
|
value={formData.nama}
|
||||||
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
onChange={(e) =>
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Posisi Organisasi</Text>}
|
setFormData({ ...formData, nama: e.target.value })
|
||||||
placeholder='Masukkan nama posisi organisasi'
|
}
|
||||||
/>
|
label="Nama Posisi Organisasi"
|
||||||
<EditEditor
|
placeholder="Masukkan nama posisi organisasi"
|
||||||
value={formData.deskripsi}
|
required
|
||||||
onChange={(htmlContent) => {
|
|
||||||
setFormData({ ...formData, deskripsi: htmlContent });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" mb={6}>
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(htmlContent) =>
|
||||||
|
setFormData({ ...formData, deskripsi: htmlContent })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
type="number"
|
||||||
value={formData.hierarki}
|
value={formData.hierarki}
|
||||||
onChange={(e) => setFormData({ ...formData, hierarki: parseInt(e.target.value) })}
|
onChange={(e) =>
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Hierarki</Text>}
|
setFormData({
|
||||||
placeholder='Masukkan hierarki'
|
...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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client';
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
|
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
|
||||||
import colors from '@/con/colors';
|
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 { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -11,47 +21,75 @@ import { useProxy } from 'valtio/utils';
|
|||||||
|
|
||||||
function CreatePosisiOrganisasi() {
|
function CreatePosisiOrganisasi() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi)
|
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateOrganisasi.findMany.load();
|
stateOrganisasi.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateOrganisasi.create.form = {
|
stateOrganisasi.create.form = {
|
||||||
nama: "",
|
nama: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
hierarki: 0, // Initialize as 0 to allow any number input
|
hierarki: 0,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await stateOrganisasi.create.submit();
|
await stateOrganisasi.create.submit();
|
||||||
resetForm();
|
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 (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header Back + Title */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button
|
||||||
</Box>
|
variant="subtle"
|
||||||
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
|
onClick={() => router.back()}
|
||||||
<Stack gap={"xs"}>
|
p="xs"
|
||||||
<Title order={3}>Create Posisi Organisasi</Title>
|
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
|
<TextInput
|
||||||
label="Nama Posisi"
|
label="Nama Posisi"
|
||||||
placeholder="Contoh: Kepala Desa"
|
placeholder="Contoh: Kepala Desa"
|
||||||
value={stateOrganisasi.create.form.nama}
|
value={stateOrganisasi.create.form.nama}
|
||||||
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
|
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<CreateEditor
|
|
||||||
value={stateOrganisasi.create.form.deskripsi}
|
<Box>
|
||||||
onChange={(htmlContent) => {
|
<Text fz={"sm"} mb={6}>Deskripsi</Text>
|
||||||
stateOrganisasi.create.form.deskripsi = htmlContent;
|
<CreateEditor
|
||||||
}}
|
value={stateOrganisasi.create.form.deskripsi}
|
||||||
/>
|
onChange={(htmlContent) => {
|
||||||
|
stateOrganisasi.create.form.deskripsi = htmlContent;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Hierarki"
|
label="Hierarki"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -63,13 +101,24 @@ function CreatePosisiOrganisasi() {
|
|||||||
stateOrganisasi.create.form.hierarki = value;
|
stateOrganisasi.create.form.hierarki = value;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
onClick={handleSubmit}
|
{/* Action Button */}
|
||||||
color="blue"
|
<Group justify="right">
|
||||||
>
|
<Button
|
||||||
Simpan
|
onClick={handleSubmit}
|
||||||
</Button>
|
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>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
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 { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
|
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
|
||||||
|
|
||||||
@@ -15,108 +32,161 @@ function PosisiOrganisasi() {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
{/* Search Bar */}
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Posisi Organisasi'
|
title="Posisi Organisasi"
|
||||||
placeholder='pencarian'
|
placeholder="Cari nama, deskripsi, atau hierarki..."
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* List Table */}
|
||||||
<ListPosisiOrganisasi search={search} />
|
<ListPosisiOrganisasi search={search} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListPosisiOrganisasi({ search }: { search: string }) {
|
function ListPosisiOrganisasi({ search }: { search: string }) {
|
||||||
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi)
|
const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
stateOrganisasi.findMany.load()
|
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 () => {
|
const handleHapus = async () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
await stateOrganisasi.delete.byId(selectedId);
|
await stateOrganisasi.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
|
load(page, 10, search); // refresh
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
|
if (loading || !data) {
|
||||||
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) {
|
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Posisi Organisasi'
|
<Title order={4}>Daftar Posisi Organisasi</Title>
|
||||||
href='/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi/create'
|
<Tooltip label="Tambah Posisi Baru" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Nama Posisi</TableTh>
|
onClick={() =>
|
||||||
<TableTh>Deskripsi</TableTh>
|
router.push(
|
||||||
<TableTh>Hierarki</TableTh>
|
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create'
|
||||||
<TableTh>Edit</TableTh>
|
)
|
||||||
<TableTh>Hapus</TableTh>
|
}
|
||||||
</TableTr>
|
>
|
||||||
</TableThead>
|
Tambah Baru
|
||||||
<TableTbody>
|
</Button>
|
||||||
{filteredData.map((item) => (
|
</Tooltip>
|
||||||
<TableTr key={item.id}>
|
</Group>
|
||||||
<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}
|
|
||||||
|
|
||||||
>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<IconTrash size={20} />
|
<Table highlightOnHover>
|
||||||
</Button>
|
<TableThead>
|
||||||
</TableTd>
|
<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>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
</TableTbody>
|
<TableTbody>
|
||||||
</Table>
|
{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>
|
</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 */}
|
{/* Modal Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { IconPhone, IconTag } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@@ -51,37 +51,44 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
Kontak Darurat
|
Kontak Darurat
|
||||||
</Title>
|
</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant='pills'
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
}}
|
borderRadius: "1rem",
|
||||||
>
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
{tabs.map((tab, i) => (
|
display: "flex",
|
||||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
flexWrap: "nowrap",
|
||||||
<TabsTab
|
gap: "0.5rem",
|
||||||
value={tab.value}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
leftSection={tab.icon}
|
}}
|
||||||
style={{
|
>
|
||||||
fontWeight: 600,
|
{tabs.map((tab, i) => (
|
||||||
fontSize: "0.9rem",
|
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
transition: "all 0.2s ease",
|
<TabsTab
|
||||||
}}
|
value={tab.value}
|
||||||
>
|
leftSection={tab.icon}
|
||||||
{tab.label}
|
style={{
|
||||||
</TabsTab>
|
fontWeight: 600,
|
||||||
</Tooltip>
|
fontSize: "0.9rem",
|
||||||
))}
|
transition: "all 0.2s ease",
|
||||||
</TabsList>
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
|
|||||||
@@ -4,19 +4,31 @@
|
|||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
|
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
|
||||||
import colors from '@/con/colors';
|
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 { DateTimePicker } from '@mantine/dates';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
export type Status = "Selesai" | "Proses" | "Gagal";
|
export type Status = "Selesai" | "Proses" | "Gagal";
|
||||||
|
|
||||||
function EditLaporanPublik() {
|
function EditLaporanPublik() {
|
||||||
const stateLaporan = useProxy(laporanPublikState)
|
const stateLaporan = useProxy(laporanPublikState);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
judul: stateLaporan.edit.form.judul || '',
|
judul: stateLaporan.edit.form.judul || '',
|
||||||
@@ -25,7 +37,7 @@ function EditLaporanPublik() {
|
|||||||
status: stateLaporan.edit.form.status || '',
|
status: stateLaporan.edit.form.status || '',
|
||||||
penanganan: stateLaporan.edit.form.penanganan || '',
|
penanganan: stateLaporan.edit.form.penanganan || '',
|
||||||
kronologi: stateLaporan.edit.form.kronologi || '',
|
kronologi: stateLaporan.edit.form.kronologi || '',
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadLaporanPublik = async () => {
|
const loadLaporanPublik = async () => {
|
||||||
@@ -46,8 +58,9 @@ function EditLaporanPublik() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading laporan publik:', error);
|
console.error('Error loading laporan publik:', error);
|
||||||
|
toast.error("Gagal mengambil data laporan publik");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
loadLaporanPublik();
|
loadLaporanPublik();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
@@ -62,75 +75,88 @@ function EditLaporanPublik() {
|
|||||||
status: formData.status,
|
status: formData.status,
|
||||||
penanganan: formData.penanganan,
|
penanganan: formData.penanganan,
|
||||||
kronologi: formData.kronologi,
|
kronologi: formData.kronologi,
|
||||||
}
|
};
|
||||||
|
|
||||||
await stateLaporan.edit.update();
|
await stateLaporan.edit.update();
|
||||||
toast.success("Laporan Publik berhasil diperbarui!");
|
toast.success("Laporan Publik berhasil diperbarui!");
|
||||||
router.push("/admin/keamanan/laporan-publik");
|
router.push("/admin/keamanan/laporan-publik");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating kontak darurat:", error);
|
console.error("Error updating laporan publik:", error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui kontak darurat");
|
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'}>
|
return (
|
||||||
<Stack gap={"xs"}>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Title order={4}>Edit Laporan Publik</Title>
|
{/* 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
|
<TextInput
|
||||||
value={formData.judul}
|
value={formData.judul}
|
||||||
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Laporan Publik</Text>}
|
label={<Text fw="bold" fz="sm">Judul Laporan Publik</Text>}
|
||||||
placeholder='Masukkan judul LaporanPublik'
|
placeholder="Masukkan judul laporan publik"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={formData.lokasi}
|
value={formData.lokasi}
|
||||||
onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Lokasi Laporan Publik</Text>}
|
label={<Text fw="bold" fz="sm">Lokasi Laporan Publik</Text>}
|
||||||
placeholder='Masukkan lokasi LaporanPublik'
|
placeholder="Masukkan lokasi laporan publik"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="Tanggal Laporan Publik"
|
label="Tanggal & Waktu Laporan Publik"
|
||||||
value={
|
value={formData.tanggalWaktu ? new Date(formData.tanggalWaktu) : null}
|
||||||
formData.tanggalWaktu
|
onChange={(val) =>
|
||||||
? new Date(formData.tanggalWaktu)
|
setFormData({ ...formData, tanggalWaktu: val ? val.toString() : '' })
|
||||||
: null
|
|
||||||
}
|
}
|
||||||
onChange={(val) => {
|
required
|
||||||
if (val) {
|
|
||||||
setFormData({ ...formData, tanggalWaktu: val.toString() });
|
|
||||||
} else {
|
|
||||||
setFormData({ ...formData, tanggalWaktu: "" }); // Reset kalau dikosongkan
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={formData.status}
|
value={formData.status}
|
||||||
onChange={(e) => setFormData({ ...formData, status: e?.valueOf() as Status })}
|
onChange={(e) => setFormData({ ...formData, status: e as Status })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Status Laporan Publik</Text>}
|
label={<Text fw="bold" fz="sm">Status Laporan Publik</Text>}
|
||||||
placeholder='Masukkan status LaporanPublik'
|
placeholder="Pilih status laporan publik"
|
||||||
data={[
|
data={[
|
||||||
{ value: "Selesai", label: "Selesai" },
|
{ value: "Selesai", label: "Selesai" },
|
||||||
{ value: "Proses", label: "Proses" },
|
{ value: "Proses", label: "Proses" },
|
||||||
{ value: "Gagal", label: "Gagal" },
|
{ value: "Gagal", label: "Gagal" },
|
||||||
]}
|
]}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={formData.kronologi}
|
value={formData.kronologi}
|
||||||
onChange={(e) => setFormData({ ...formData, kronologi: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, kronologi: e.target.value })}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kronologi Laporan Publik</Text>}
|
label={<Text fw="bold" fz="sm">Kronologi Laporan Publik</Text>}
|
||||||
placeholder='Masukkan kronologi LaporanPublik'
|
placeholder="Masukkan kronologi laporan publik"
|
||||||
/>
|
/>
|
||||||
<Text fw={"bold"} fz={"sm"}>Penanganan Laporan Publik</Text>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Laporan Publik</Text>
|
<Text fw="bold" fz="sm" mb={6}>Penanganan Laporan Publik</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.penanganan}
|
value={formData.penanganan}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -139,8 +165,20 @@ function EditLaporanPublik() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,126 +1,173 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { 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 { useParams, useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import laporanPublikState from '../../../_state/keamanan/laporan-publik';
|
import laporanPublikState from '../../../_state/keamanan/laporan-publik';
|
||||||
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
|
||||||
function DetailLaporanPublik() {
|
function DetailLaporanPublik() {
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const stateLaporan = useProxy(laporanPublikState)
|
const stateLaporan = useProxy(laporanPublikState);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
stateLaporan.findUnique.load(params?.id as string)
|
stateLaporan.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
stateLaporan.delete.byId(selectedId)
|
stateLaporan.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/keamanan/laporan-publik")
|
router.push('/admin/keamanan/laporan-publik');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!stateLaporan.findUnique.data) {
|
if (!stateLaporan.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = stateLaporan.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
{/* Tombol Kembali */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
variant="subtle"
|
||||||
</Button>
|
onClick={() => router.back()}
|
||||||
</Box>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
|
mb={15}
|
||||||
<Stack>
|
>
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Laporan Publik</Text>
|
Kembali
|
||||||
<Paper bg={colors['BG-trans']} p={'md'}>
|
</Button>
|
||||||
<Stack gap={"xs"}>
|
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
w={{ base: '100%', md: '60%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Laporan Publik
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Judul Laporan Publik</Text>
|
<Text fz="lg" fw="bold">Judul Laporan Publik</Text>
|
||||||
<Text fz={"lg"}>{stateLaporan.findUnique.data?.judul}</Text>
|
<Text fz="md" c="dimmed">{data.judul || '-'}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Tanggal Laporan Publik</Text>
|
<Text fz="lg" fw="bold">Tanggal Laporan Publik</Text>
|
||||||
<Text fz={"lg"}>
|
<Text fz="md" c="dimmed">
|
||||||
{stateLaporan.findUnique.data?.tanggalWaktu
|
{data.tanggalWaktu
|
||||||
? new Date(stateLaporan.findUnique.data.tanggalWaktu).toLocaleString('id-ID')
|
? new Date(data.tanggalWaktu).toLocaleString('id-ID')
|
||||||
: '-'}
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Lokasi</Text>
|
<Text fz="lg" fw="bold">Lokasi</Text>
|
||||||
<Text fz={"lg"}>{stateLaporan.findUnique.data?.lokasi}</Text>
|
<Text fz="md" c="dimmed">{data.lokasi || '-'}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Status</Text>
|
<Text fz="lg" fw="bold">Status</Text>
|
||||||
<Text fz={"lg"}>{stateLaporan.findUnique.data?.status}</Text>
|
<Text fz="md" c="dimmed">{data.status || '-'}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Kronologi</Text>
|
<Text fz="lg" fw="bold">Kronologi</Text>
|
||||||
<Text fz={"lg"}>
|
<Text fz="md" c="dimmed">{data.kronologi || '-'}</Text>
|
||||||
{stateLaporan.findUnique.data?.kronologi || '-'}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Text fz={"lg"} fw={"bold"}>Penanganan</Text>
|
|
||||||
{stateLaporan.findUnique.data?.penanganan?.map((item, index) => (
|
<Box>
|
||||||
<Box key={index}>
|
<Text fz="lg" fw="bold">Penanganan</Text>
|
||||||
<Text fz={"lg"} fw={"bold"}>Deskripsi Penanganan</Text>
|
{data.penanganan?.length ? (
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
|
data.penanganan.map((item, index) => (
|
||||||
</Box>
|
<Box key={index} pl="sm">
|
||||||
))}
|
<Text fz="md" fw="bold">Deskripsi Penanganan</Text>
|
||||||
{!stateLaporan.findUnique.data?.penanganan?.length && (
|
<Text
|
||||||
<Text fz={"lg"} fs="italic">Belum ada penanganan</Text>
|
fz="md"
|
||||||
)}
|
c="dimmed"
|
||||||
<Flex gap={"xs"} mt={10}>
|
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
|
||||||
<Button
|
/>
|
||||||
onClick={() => {
|
</Box>
|
||||||
if (stateLaporan.findUnique.data) {
|
))
|
||||||
setSelectedId(stateLaporan.findUnique.data.id);
|
) : (
|
||||||
|
<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);
|
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`)
|
||||||
}
|
}
|
||||||
}}
|
variant="light"
|
||||||
disabled={stateLaporan.delete.loading || !stateLaporan.findUnique.data}
|
radius="md"
|
||||||
color={"red"}
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</Tooltip>
|
||||||
onClick={() => {
|
</Group>
|
||||||
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>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
{/* Modal Konfirmasi Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
|
||||||
opened={modalHapus}
|
|
||||||
onClose={() => setModalHapus(false)}
|
|
||||||
onConfirm={handleDelete}
|
|
||||||
text='Apakah anda yakin ingin menghapus laporan publik ini?'
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* Modal Konfirmasi Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
text="Apakah anda yakin ingin menghapus laporan publik ini?"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +1,142 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import colors from '@/con/colors';
|
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 { DateTimePicker } from '@mantine/dates';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import CreateEditor from '../../../_com/createEditor';
|
import CreateEditor from '../../../_com/createEditor';
|
||||||
import laporanPublikState from '../../../_state/keamanan/laporan-publik';
|
import laporanPublikState from '../../../_state/keamanan/laporan-publik';
|
||||||
export type Status = "Selesai" | "Proses" | "Gagal";
|
|
||||||
|
export type Status = 'Selesai' | 'Proses' | 'Gagal';
|
||||||
|
|
||||||
function CreateLaporanPublik() {
|
function CreateLaporanPublik() {
|
||||||
const stateLaporan = useProxy(laporanPublikState)
|
const stateLaporan = useProxy(laporanPublikState);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateLaporan.create.form = {
|
stateLaporan.create.form = {
|
||||||
judul: "",
|
judul: '',
|
||||||
lokasi: "",
|
lokasi: '',
|
||||||
tanggalWaktu: "",
|
tanggalWaktu: '',
|
||||||
status: "Proses" as Status,
|
status: 'Proses' as Status,
|
||||||
penanganan: "",
|
penanganan: '',
|
||||||
kronologi: "",
|
kronologi: '',
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await stateLaporan.create.create();
|
await stateLaporan.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push('/admin/keamanan/laporan-publik');
|
router.push('/admin/keamanan/laporan-publik');
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header with Back Button */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<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'}>
|
{/* Form Card */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Laporan Publik</Title>
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateLaporan.create.form.judul}
|
value={stateLaporan.create.form.judul}
|
||||||
onChange={(e) => stateLaporan.create.form.judul = e.target.value}
|
onChange={(e) => (stateLaporan.create.form.judul = e.target.value)}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Judul Laporan Publik</Text>}
|
label={<Text fw="bold" fz="sm">Judul Laporan Publik</Text>}
|
||||||
placeholder='Masukkan judul LaporanPublik'
|
placeholder="Masukkan judul laporan publik"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateLaporan.create.form.lokasi}
|
value={stateLaporan.create.form.lokasi}
|
||||||
onChange={(e) => stateLaporan.create.form.lokasi = e.target.value}
|
onChange={(e) => (stateLaporan.create.form.lokasi = e.target.value)}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Lokasi Laporan Publik</Text>}
|
label={<Text fw="bold" fz="sm">Lokasi Laporan Publik</Text>}
|
||||||
placeholder='Masukkan lokasi LaporanPublik'
|
placeholder="Masukkan lokasi laporan publik"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="Tanggal Laporan Publik"
|
label={<Text fw="bold" fz="sm">Tanggal Laporan Publik</Text>}
|
||||||
value={
|
value={
|
||||||
stateLaporan.create.form.tanggalWaktu
|
stateLaporan.create.form.tanggalWaktu
|
||||||
? new Date(stateLaporan.create.form.tanggalWaktu)
|
? new Date(stateLaporan.create.form.tanggalWaktu)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) {
|
stateLaporan.create.form.tanggalWaktu = val ? val.toString() : '';
|
||||||
stateLaporan.create.form.tanggalWaktu = val.toString();
|
|
||||||
} else {
|
|
||||||
stateLaporan.create.form.tanggalWaktu = ""; // Reset kalau dikosongkan
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={stateLaporan.create.form.status}
|
value={stateLaporan.create.form.status}
|
||||||
onChange={(e) => stateLaporan.create.form.status = e?.valueOf() as Status}
|
onChange={(e) => (stateLaporan.create.form.status = e?.valueOf() as Status)}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Status Laporan Publik</Text>}
|
label={<Text fw="bold" fz="sm">Status Laporan Publik</Text>}
|
||||||
placeholder='Masukkan status LaporanPublik'
|
placeholder="Pilih status laporan publik"
|
||||||
data={[
|
data={[
|
||||||
{ value: "Selesai", label: "Selesai" },
|
{ value: 'Selesai', label: 'Selesai' },
|
||||||
{ value: "Proses", label: "Proses" },
|
{ value: 'Proses', label: 'Proses' },
|
||||||
{ value: "Gagal", label: "Gagal" },
|
{ value: 'Gagal', label: 'Gagal' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateLaporan.create.form.kronologi}
|
value={stateLaporan.create.form.kronologi}
|
||||||
onChange={(e) => stateLaporan.create.form.kronologi = e.target.value}
|
onChange={(e) => (stateLaporan.create.form.kronologi = e.target.value)}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Kronologi Laporan Publik</Text>}
|
label={<Text fw="bold" fz="sm">Kronologi Laporan Publik</Text>}
|
||||||
placeholder='Masukkan kronologi LaporanPublik'
|
placeholder="Masukkan kronologi laporan publik"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<Text fw={"bold"} fz={"sm"}>Penanganan Laporan Publik</Text>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Laporan Publik</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Penanganan Laporan Publik
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={stateLaporan.create.form.penanganan}
|
value={stateLaporan.create.form.penanganan}
|
||||||
onChange={(e) => stateLaporan.create.form.penanganan = e}
|
onChange={(e) => (stateLaporan.create.form.penanganan = e)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
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 HeaderSearch from '../../_com/header';
|
||||||
import JudulList from '../../_com/judulList';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import laporanPublikState from '../../_state/keamanan/laporan-publik';
|
import laporanPublikState from '../../_state/keamanan/laporan-publik';
|
||||||
@@ -16,74 +33,134 @@ function LaporanPublik() {
|
|||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Laporan Publik'
|
title='Laporan Publik'
|
||||||
placeholder='pencarian'
|
placeholder='Cari laporan publik...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<ListLaporanPublik search={search}/>
|
<ListLaporanPublik search={search} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListLaporanPublik({ search }: { search: string }) {
|
function ListLaporanPublik({ search }: { search: string }) {
|
||||||
const stateLaporan = useProxy(laporanPublikState)
|
const stateLaporan = useProxy(laporanPublikState);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = stateLaporan.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
stateLaporan.findMany.load()
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = (stateLaporan.findMany.data || []).filter(item => {
|
const filteredData = data || [];
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.judul.toLowerCase().includes(keyword) ||
|
|
||||||
item.status.toLowerCase().includes(keyword) ||
|
|
||||||
item.kronologi?.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!stateLaporan.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Laporan Publik'
|
<Title order={4}>Daftar Laporan Publik</Title>
|
||||||
href='/admin/keamanan/laporan-publik/create'
|
<Tooltip label="Tambah Laporan Publik" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Judul Laporan Publik</TableTh>
|
onClick={() => router.push('/admin/keamanan/laporan-publik/create')}
|
||||||
<TableTh>Tanggal Laporan Publik</TableTh>
|
>
|
||||||
<TableTh>Status</TableTh>
|
Tambah Baru
|
||||||
<TableTh>Deskripsi</TableTh>
|
</Button>
|
||||||
<TableTh>Detail</TableTh>
|
</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>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.length > 0 ? (
|
||||||
<TableTr key={item.id}>
|
filteredData.map((item) => (
|
||||||
<TableTd>{item.judul}</TableTd>
|
<TableTr key={item.id}>
|
||||||
<TableTd>{new Date(item.tanggalWaktu).toLocaleDateString('id-ID')}</TableTd>
|
<TableTd>
|
||||||
<TableTd>{item.status}</TableTd>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
<TableTd>{item.kronologi}</TableTd>
|
{item.judul}
|
||||||
<TableTd>
|
</Text>
|
||||||
<Button onClick={() => router.push(`/admin/keamanan/laporan-publik/${item.id}`)}>
|
</TableTd>
|
||||||
<IconDeviceImac size={20} />
|
<TableTd>
|
||||||
</Button>
|
<Text fz="sm" c="dimmed">
|
||||||
</TableTd>
|
{new Date(item.tanggalWaktu).toLocaleDateString('id-ID')}
|
||||||
</TableTr>
|
</Text>
|
||||||
))}
|
</TableTd>
|
||||||
</TableTbody>
|
<TableTd>
|
||||||
</Table>
|
<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>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,22 +4,27 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Center,
|
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title,
|
||||||
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} 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 { useParams, useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useProxy } from "valtio/utils";
|
import { useProxy } from "valtio/utils";
|
||||||
|
|
||||||
|
|
||||||
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
@@ -34,24 +39,24 @@ function EditTipsKeamanan() {
|
|||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
judul: keamananState.update.form.judul || '',
|
judul: keamananState.update.form.judul || "",
|
||||||
deskripsi: keamananState.update.form.deskripsi || '',
|
deskripsi: keamananState.update.form.deskripsi || "",
|
||||||
imageId: keamananState.update.form.imageId || ''
|
imageId: keamananState.update.form.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load berita by id saat pertama kali
|
// Load data saat pertama kali
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadBerita = async () => {
|
const loadData = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await keamananState.update.load(id); // akses langsung, bukan dari proxy
|
const data = await keamananState.update.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
setFormData({
|
setFormData({
|
||||||
judul: data.judul || '',
|
judul: data.judul || "",
|
||||||
deskripsi: data.deskripsi || '',
|
deskripsi: data.deskripsi || "",
|
||||||
imageId: data.imageId || '',
|
imageId: data.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data?.image?.link) {
|
if (data?.image?.link) {
|
||||||
@@ -59,35 +64,30 @@ function EditTipsKeamanan() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading keamananLingkungan:", error);
|
console.error("Error loading tips keamanan:", error);
|
||||||
toast.error("Gagal memuat data keamananLingkungan");
|
toast.error("Gagal memuat data tips keamanan");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadBerita();
|
loadData();
|
||||||
}, [params?.id]); // ✅ hapus beritaState dari dependency
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update global state with form data
|
|
||||||
keamananState.update.form = {
|
keamananState.update.form = {
|
||||||
...keamananState.update.form,
|
...keamananState.update.form,
|
||||||
judul: formData.judul,
|
...formData,
|
||||||
deskripsi: formData.deskripsi,
|
|
||||||
imageId: formData.imageId // Keep existing imageId if not changed
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Jika ada file baru, upload
|
|
||||||
if (file) {
|
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;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) return toast.error("Gagal upload gambar");
|
||||||
return toast.error("Gagal upload gambar");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update imageId in global state
|
|
||||||
keamananState.update.form.imageId = uploaded.id;
|
keamananState.update.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,71 +95,119 @@ function EditTipsKeamanan() {
|
|||||||
toast.success("Tips Keamanan berhasil diperbarui!");
|
toast.success("Tips Keamanan berhasil diperbarui!");
|
||||||
router.push("/admin/keamanan/tips-keamanan");
|
router.push("/admin/keamanan/tips-keamanan");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating keamananLingkungan:", error);
|
console.error("Error updating tips keamanan:", error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui keamananLingkungan");
|
toast.error("Terjadi kesalahan saat memperbarui tips keamanan");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={30} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
|
</Button>
|
||||||
<Stack gap={"xs"}>
|
</Tooltip>
|
||||||
<Title order={3}>Edit Tips Keamanan</Title>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Dropzone
|
Edit Tips Keamanan
|
||||||
onDrop={(files) => {
|
</Title>
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
</Group>
|
||||||
if (selectedFile) {
|
|
||||||
setFile(selectedFile);
|
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
|
||||||
accept={{ 'image/*': [] }}
|
|
||||||
>
|
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
|
||||||
<Dropzone.Accept>
|
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
|
||||||
</Dropzone.Accept>
|
|
||||||
<Dropzone.Reject>
|
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
|
||||||
</Dropzone.Reject>
|
|
||||||
<Dropzone.Idle>
|
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
|
||||||
</Dropzone.Idle>
|
|
||||||
|
|
||||||
<div>
|
{/* Form Card */}
|
||||||
<Text size="xl" inline>
|
<Paper
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
w={{ base: "100%", md: "60%" }}
|
||||||
</Text>
|
bg={colors["white-1"]}
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
p="lg"
|
||||||
Maksimal 5MB dan harus format gambar
|
radius="md"
|
||||||
</Text>
|
shadow="sm"
|
||||||
</div>
|
style={{ border: "1px solid #e0e0e0" }}
|
||||||
</Group>
|
>
|
||||||
</Dropzone>
|
<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 ? (
|
{previewImage ? (
|
||||||
<Image alt="" src={previewImage} w={200} h={200} />
|
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
|
||||||
) : (
|
<Image
|
||||||
<Center w={200} h={200} bg={"gray"}>
|
src={previewImage}
|
||||||
<IconImageInPicture />
|
alt="Preview Gambar"
|
||||||
</Center>
|
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
|
<TextInput
|
||||||
|
label="Nama Tips Keamanan"
|
||||||
|
placeholder="Masukkan nama tips keamanan"
|
||||||
value={formData.judul}
|
value={formData.judul}
|
||||||
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
|
||||||
label={<Text fz={"sm"} fw={"bold"}>Nama Tips Keamanan</Text>}
|
required
|
||||||
placeholder="masukkan nama tips keamanan"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Input Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(htmlContent) => {
|
onChange={(htmlContent) => {
|
||||||
@@ -169,7 +217,21 @@ function EditTipsKeamanan() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</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>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
||||||
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
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 { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -11,101 +10,134 @@ import colors from '@/con/colors';
|
|||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import tipsKeamananState from '../../../_state/keamanan/tips-keamanan';
|
import tipsKeamananState from '../../../_state/keamanan/tips-keamanan';
|
||||||
|
|
||||||
|
|
||||||
function DetailTipsKeamanan() {
|
function DetailTipsKeamanan() {
|
||||||
const stateKeamanan = useProxy(tipsKeamananState)
|
const stateKeamanan = useProxy(tipsKeamananState);
|
||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const params = useParams()
|
const params = useParams();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
stateKeamanan.findUnique.load(params?.id as string)
|
stateKeamanan.findUnique.load(params?.id as string);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
stateKeamanan.delete.byId(selectedId)
|
stateKeamanan.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
router.push("/admin/keamanan/tips-keamanan")
|
router.push("/admin/keamanan/tips-keamanan");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!stateKeamanan.findUnique.data) {
|
if (!stateKeamanan.findUnique.data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={40} />
|
<Skeleton height={500} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = stateKeamanan.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box py={10}>
|
||||||
<Box mb={10}>
|
<Button
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
variant="subtle"
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
onClick={() => router.back()}
|
||||||
</Button>
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
</Box>
|
mb={15}
|
||||||
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
|
>
|
||||||
<Stack>
|
Kembali
|
||||||
<Text fz={"xl"} fw={"bold"}>Detail Tips Keamanan</Text>
|
</Button>
|
||||||
{stateKeamanan.findUnique.data ? (
|
|
||||||
<Paper key={stateKeamanan.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
|
<Paper
|
||||||
<Stack gap={"xs"}>
|
withBorder
|
||||||
<Box>
|
w={{ base: "100%", md: "60%" }}
|
||||||
<Text fw={"bold"} fz={"lg"}>Nama Tips Keamanan</Text>
|
bg={colors['white-1']}
|
||||||
<Text fz={"lg"}>{stateKeamanan.findUnique.data?.judul}</Text>
|
p="lg"
|
||||||
</Box>
|
radius="md"
|
||||||
<Box>
|
shadow="sm"
|
||||||
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
|
>
|
||||||
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateKeamanan.findUnique.data?.deskripsi }} />
|
<Stack gap="md">
|
||||||
</Box>
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
<Box>
|
Detail Tips Keamanan
|
||||||
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
|
</Text>
|
||||||
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateKeamanan.findUnique.data?.image?.link} alt="gambar" />
|
|
||||||
</Box>
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
<Flex gap={"xs"} mt={10}>
|
<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
|
<Button
|
||||||
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (stateKeamanan.findUnique.data) {
|
setSelectedId(data.id);
|
||||||
setSelectedId(stateKeamanan.findUnique.data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={stateKeamanan.delete.loading || !stateKeamanan.findUnique.data}
|
variant="light"
|
||||||
color={"red"}
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
disabled={stateKeamanan.delete.loading}
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Edit Tips Keamanan" withArrow position="top">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
color="green"
|
||||||
if (stateKeamanan.findUnique.data) {
|
onClick={() => router.push(`/admin/keamanan/tips-keamanan/${data.id}/edit`)}
|
||||||
router.push(`/admin/keamanan/tips-keamanan/${stateKeamanan.findUnique.data.id}/edit`);
|
variant="light"
|
||||||
}
|
radius="md"
|
||||||
}}
|
size="md"
|
||||||
disabled={!stateKeamanan.findUnique.data}
|
|
||||||
color={"green"}
|
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Tooltip>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
) : null}
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Modal Konfirmasi Hapus */}
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
onConfirm={handleHapus}
|
onConfirm={handleHapus}
|
||||||
text='Apakah anda yakin ingin menghapus tips keamanan ini?'
|
text="Apakah anda yakin ingin menghapus tips keamanan ini?"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DetailTipsKeamanan;
|
export default DetailTipsKeamanan;
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
'use client'
|
'use client';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
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 { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -11,37 +22,36 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import CreateEditor from '../../../_com/createEditor';
|
import CreateEditor from '../../../_com/createEditor';
|
||||||
import tipsKeamananState from '../../../_state/keamanan/tips-keamanan';
|
import tipsKeamananState from '../../../_state/keamanan/tips-keamanan';
|
||||||
|
|
||||||
|
|
||||||
function CreateKeamananLingkungan() {
|
function CreateKeamananLingkungan() {
|
||||||
const stateKeamanan = useProxy(tipsKeamananState)
|
const stateKeamanan = useProxy(tipsKeamananState);
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
stateKeamanan.create.form = {
|
stateKeamanan.create.form = {
|
||||||
judul: "",
|
judul: '',
|
||||||
deskripsi: "",
|
deskripsi: '',
|
||||||
imageId: "",
|
imageId: '',
|
||||||
}
|
};
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
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({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
})
|
});
|
||||||
|
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error("Gagal mengupload file");
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
}
|
}
|
||||||
|
|
||||||
stateKeamanan.create.form.imageId = uploaded.id;
|
stateKeamanan.create.form.imageId = uploaded.id;
|
||||||
@@ -49,86 +59,94 @@ function CreateKeamananLingkungan() {
|
|||||||
await stateKeamanan.create.create();
|
await stateKeamanan.create.create();
|
||||||
|
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/keamanan/tips-keamanan")
|
router.push('/admin/keamanan/tips-keamanan');
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Box mb={10}>
|
{/* Header Back + Title */}
|
||||||
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
|
<Group mb="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
||||||
</Button>
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
</Box>
|
<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'}>
|
{/* Form Card */}
|
||||||
<Stack gap={"xs"}>
|
<Paper
|
||||||
<Title order={4}>Create Tips Keamanan</Title>
|
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>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
<Box>
|
Gambar Tips Keamanan
|
||||||
<Dropzone
|
</Text>
|
||||||
onDrop={(files) => {
|
<Dropzone
|
||||||
const selectedFile = files[0]; // Ambil file pertama
|
onDrop={(files) => {
|
||||||
if (selectedFile) {
|
const selectedFile = files[0];
|
||||||
setFile(selectedFile);
|
if (selectedFile) {
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
|
setFile(selectedFile);
|
||||||
}
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}}
|
}
|
||||||
onReject={() => toast.error('File tidak valid.')}
|
}}
|
||||||
maxSize={5 * 1024 ** 2} // Maks 5MB
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
accept={{ 'image/*': [] }}
|
maxSize={5 * 1024 ** 2}
|
||||||
>
|
accept={{ 'image/*': [] }}
|
||||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
|
radius="md"
|
||||||
<Dropzone.Accept>
|
p="xl"
|
||||||
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
>
|
||||||
</Dropzone.Accept>
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
<Dropzone.Reject>
|
<Dropzone.Accept>
|
||||||
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
</Dropzone.Reject>
|
</Dropzone.Accept>
|
||||||
<Dropzone.Idle>
|
<Dropzone.Reject>
|
||||||
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</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>
|
{previewImage && (
|
||||||
<Text size="xl" inline>
|
<Box mt="sm" style={{ textAlign: 'center' }}>
|
||||||
Drag gambar ke sini atau klik untuk pilih file
|
<Image
|
||||||
</Text>
|
src={previewImage}
|
||||||
<Text size="sm" c="dimmed" inline mt={7}>
|
alt="Preview Gambar"
|
||||||
Maksimal 5MB dan harus format gambar
|
radius="md"
|
||||||
</Text>
|
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
|
||||||
</div>
|
/>
|
||||||
</Group>
|
</Box>
|
||||||
</Dropzone>
|
)}
|
||||||
|
|
||||||
{/* Tampilkan preview kalau ada */}
|
|
||||||
{previewImage && (
|
|
||||||
<Box mt="sm">
|
|
||||||
<Image
|
|
||||||
src={previewImage}
|
|
||||||
alt="Preview"
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '200px',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Input Judul */}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={stateKeamanan.create.form.judul}
|
label="Judul Tips Keamanan"
|
||||||
onChange={(val) => {
|
placeholder="Masukkan judul tips keamanan"
|
||||||
stateKeamanan.create.form.judul = val.target.value;
|
value={stateKeamanan.create.form.judul || ''}
|
||||||
}}
|
onChange={(e) => (stateKeamanan.create.form.judul = e.target.value)}
|
||||||
label={<Text fw={"bold"} fz={"sm"}>Nama Tips Keamanan</Text>}
|
required
|
||||||
placeholder='Masukkan nama Tips Keamanan'
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Editor Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Tips Keamanan
|
||||||
|
</Text>
|
||||||
<CreateEditor
|
<CreateEditor
|
||||||
value={stateKeamanan.create.form.deskripsi}
|
value={stateKeamanan.create.form.deskripsi}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
@@ -136,8 +154,21 @@ function CreateKeamananLingkungan() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,14 +1,31 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
import {
|
||||||
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
Box,
|
||||||
import HeaderSearch from '../../_com/header';
|
Button,
|
||||||
import JudulList from '../../_com/judulList';
|
Center,
|
||||||
import { useRouter } from 'next/navigation';
|
Group,
|
||||||
import tipsKeamananState from '../../_state/keamanan/tips-keamanan';
|
Pagination,
|
||||||
import { useProxy } from 'valtio/utils';
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
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 { useState } from 'react';
|
||||||
|
import HeaderSearch from '../../_com/header';
|
||||||
|
import tipsKeamananState from '../../_state/keamanan/tips-keamanan';
|
||||||
|
|
||||||
function TipsKeamanan() {
|
function TipsKeamanan() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -16,12 +33,12 @@ function TipsKeamanan() {
|
|||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Tips Keamanan'
|
title='Tips Keamanan'
|
||||||
placeholder='pencarian'
|
placeholder='Cari tips keamanan...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<ListTipsKeamanan search={search}/>
|
<ListTipsKeamanan search={search} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -47,54 +64,85 @@ function ListTipsKeamanan({ search }: { search: string }) {
|
|||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={10}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Tips Keamanan'
|
<Title order={4}>Daftar Tips Keamanan</Title>
|
||||||
href='/admin/keamanan/tips-keamanan/create'
|
<Tooltip label="Tambah Tips Keamanan" withArrow>
|
||||||
/>
|
<Button
|
||||||
<Table striped withTableBorder withRowBorders>
|
leftSection={<IconPlus size={18} />}
|
||||||
<TableThead>
|
color="blue"
|
||||||
<TableTr>
|
variant="light"
|
||||||
<TableTh>Nama Tips Keamanan</TableTh>
|
onClick={() => router.push('/admin/keamanan/tips-keamanan/create')}
|
||||||
<TableTh>Deskripsi</TableTh>
|
>
|
||||||
<TableTh>Detail</TableTh>
|
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>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{filteredData.length > 0 ? (
|
||||||
<TableTr key={item.id}>
|
filteredData.map((item) => (
|
||||||
<TableTd>
|
<TableTr key={item.id}>
|
||||||
<Box w={150}>
|
<TableTd style={{ width: '25%' }}>
|
||||||
<Text fz={"md"} truncate={"end"} lineClamp={1}>{item.judul}</Text>
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
</Box>
|
{item.judul}
|
||||||
</TableTd>
|
</Text>
|
||||||
<TableTd>
|
</TableTd>
|
||||||
<Box w={250}>
|
<TableTd style={{ width: '45%' }}>
|
||||||
<Text fz={"md"} truncate={"end"} lineClamp={1} dangerouslySetInnerHTML={{__html: item.deskripsi}} />
|
<Text fz="sm" c="dimmed" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
</Box>
|
</TableTd>
|
||||||
</TableTd>
|
<TableTd style={{ width: '15%' }}>
|
||||||
<TableTd>
|
<Button
|
||||||
<Button onClick={() => router.push(`/admin/keamanan/tips-keamanan/${item.id}`)}>
|
variant="light"
|
||||||
<IconDeviceImac size={20} />
|
color="blue"
|
||||||
</Button>
|
onClick={() => router.push(`/admin/keamanan/tips-keamanan/${item.id}`)}
|
||||||
</TableTd>
|
>
|
||||||
</TableTr>
|
<IconDeviceImac size={20} />
|
||||||
))}
|
<Text ml={5}>Detail</Text>
|
||||||
</TableTbody>
|
</Button>
|
||||||
</Table>
|
</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>
|
</Paper>
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)}
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10)
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
my="md"
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,139 +1,146 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { IconActivity, IconBuildingHospital, IconCalendarEvent, IconGauge, IconNotes } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
|
||||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "Presentase Kelahiran & Kematian",
|
label: "Presentase Kelahiran & Kematian",
|
||||||
value: "presentasekelahiran&kematian",
|
value: "presentasekelahiran&kematian",
|
||||||
href: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian",
|
href: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian",
|
||||||
icon: <IconActivity size={18} stroke={1.8} />,
|
icon: <IconActivity size={18} stroke={1.8} />,
|
||||||
tooltip: "Lihat data kelahiran dan kematian"
|
tooltip: "Lihat data kelahiran dan kematian"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Grafik Hasil Kepuasan Masyarakat",
|
label: "Grafik Hasil Kepuasan Masyarakat",
|
||||||
value: "grafikhasilkepuasan",
|
value: "grafikhasilkepuasan",
|
||||||
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
|
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
|
||||||
icon: <IconGauge size={18} stroke={1.8} />,
|
icon: <IconGauge size={18} stroke={1.8} />,
|
||||||
tooltip: "Grafik kepuasan masyarakat terhadap pelayanan"
|
tooltip: "Grafik kepuasan masyarakat terhadap pelayanan"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Fasilitas Kesehatan",
|
label: "Fasilitas Kesehatan",
|
||||||
value: "fasilitaskesehatan",
|
value: "fasilitaskesehatan",
|
||||||
href: "/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan",
|
href: "/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan",
|
||||||
icon: <IconBuildingHospital size={18} stroke={1.8} />,
|
icon: <IconBuildingHospital size={18} stroke={1.8} />,
|
||||||
tooltip: "Data fasilitas kesehatan desa"
|
tooltip: "Data fasilitas kesehatan desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Jadwal Kegiatan",
|
label: "Jadwal Kegiatan",
|
||||||
value: "jadwalkegiatan",
|
value: "jadwalkegiatan",
|
||||||
href: "/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan",
|
href: "/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan",
|
||||||
icon: <IconCalendarEvent size={18} stroke={1.8} />,
|
icon: <IconCalendarEvent size={18} stroke={1.8} />,
|
||||||
tooltip: "Atur jadwal kegiatan kesehatan"
|
tooltip: "Atur jadwal kegiatan kesehatan"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Artikel Kesehatan",
|
label: "Artikel Kesehatan",
|
||||||
value: "artikelkesehatan",
|
value: "artikelkesehatan",
|
||||||
href: "/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan",
|
href: "/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan",
|
||||||
icon: <IconNotes size={18} stroke={1.8} />,
|
icon: <IconNotes size={18} stroke={1.8} />,
|
||||||
tooltip: "Artikel & informasi seputar kesehatan"
|
tooltip: "Artikel & informasi seputar kesehatan"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
const currentTab = tabs.find(tab => tab.href === pathname);
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
||||||
|
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value);
|
const tab = tabs.find(t => t.value === value);
|
||||||
if (tab) {
|
if (tab) {
|
||||||
router.push(tab.href);
|
router.push(tab.href);
|
||||||
}
|
}
|
||||||
setActiveTab(value);
|
setActiveTab(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const match = tabs.find(tab => tab.href === pathname);
|
const match = tabs.find(tab => tab.href === pathname);
|
||||||
if (match) {
|
if (match) {
|
||||||
setActiveTab(match.value);
|
setActiveTab(match.value);
|
||||||
}
|
}
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||||
Data Kesehatan Warga
|
Data Kesehatan Warga
|
||||||
</Title>
|
</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant="pills"
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
}}
|
borderRadius: "1rem",
|
||||||
>
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
{tabs.map((tab, i) => (
|
display: "flex",
|
||||||
<Tooltip
|
flexWrap: "nowrap",
|
||||||
key={i}
|
gap: "0.5rem",
|
||||||
label={tab.tooltip}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
position="bottom"
|
}}
|
||||||
withArrow
|
>
|
||||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
{tabs.map((tab, i) => (
|
||||||
>
|
<Tooltip
|
||||||
<TabsTab
|
key={i}
|
||||||
value={tab.value}
|
label={tab.tooltip}
|
||||||
leftSection={tab.icon}
|
position="bottom"
|
||||||
style={{
|
withArrow
|
||||||
fontWeight: 600,
|
transitionProps={{ transition: 'pop', duration: 200 }}
|
||||||
fontSize: "0.9rem",
|
>
|
||||||
transition: "all 0.2s ease",
|
<TabsTab
|
||||||
}}
|
value={tab.value}
|
||||||
>
|
leftSection={tab.icon}
|
||||||
{tab.label}
|
style={{
|
||||||
</TabsTab>
|
fontWeight: 600,
|
||||||
</Tooltip>
|
fontSize: "0.9rem",
|
||||||
))}
|
transition: "all 0.2s ease",
|
||||||
</TabsList>
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
key={i}
|
key={i}
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
style={{
|
style={{
|
||||||
padding: "1.5rem",
|
padding: "1.5rem",
|
||||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
"use client";
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import React, { useEffect, useState } from "react";
|
||||||
import { IconList, IconCategory } from '@tabler/icons-react';
|
import {
|
||||||
import colors from '@/con/colors';
|
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 }) {
|
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -27,19 +37,21 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
const currentTab = tabs.find((tab) => tab.href === pathname);
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
const [activeTab, setActiveTab] = useState<string | null>(
|
||||||
|
currentTab?.value || tabs[0].value
|
||||||
|
);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value);
|
const tab = tabs.find((t) => t.value === value);
|
||||||
if (tab) {
|
if (tab) {
|
||||||
router.push(tab.href);
|
router.push(tab.href);
|
||||||
}
|
}
|
||||||
setActiveTab(value)
|
setActiveTab(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const match = tabs.find(tab => tab.href === pathname);
|
const match = tabs.find((tab) => tab.href === pathname);
|
||||||
if (match) {
|
if (match) {
|
||||||
setActiveTab(match.value);
|
setActiveTab(match.value);
|
||||||
}
|
}
|
||||||
@@ -51,37 +63,51 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
Desa Anti Korupsi
|
Desa Anti Korupsi
|
||||||
</Title>
|
</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant="pills"
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
}}
|
borderRadius: "1rem",
|
||||||
>
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
{tabs.map((tab, i) => (
|
display: "flex",
|
||||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
flexWrap: "nowrap",
|
||||||
<TabsTab
|
gap: "0.5rem",
|
||||||
value={tab.value}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
leftSection={tab.icon}
|
}}
|
||||||
style={{
|
>
|
||||||
fontWeight: 600,
|
{tabs.map((tab, i) => (
|
||||||
fontSize: "0.9rem",
|
<Tooltip
|
||||||
transition: "all 0.2s ease",
|
key={i}
|
||||||
}}
|
label={tab.tooltip}
|
||||||
|
position="bottom"
|
||||||
|
withArrow
|
||||||
|
transitionProps={{ transition: "pop", duration: 200 }}
|
||||||
>
|
>
|
||||||
{tab.label}
|
<TabsTab
|
||||||
</TabsTab>
|
value={tab.value}
|
||||||
</Tooltip>
|
leftSection={tab.icon}
|
||||||
))}
|
style={{
|
||||||
</TabsList>
|
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) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
@@ -102,4 +128,4 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabs;
|
export default LayoutTabs;
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover striped verticalSpacing="sm">
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama Kategori</TableTh>
|
<TableTh>Nama Kategori</TableTh>
|
||||||
|
|||||||
@@ -75,64 +75,65 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Group>
|
</Group>
|
||||||
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
<Table
|
||||||
|
striped
|
||||||
|
highlightOnHover
|
||||||
|
|
||||||
<Table
|
withRowBorders
|
||||||
striped
|
verticalSpacing="sm"
|
||||||
highlightOnHover
|
>
|
||||||
|
<TableThead>
|
||||||
withRowBorders
|
<TableTr>
|
||||||
verticalSpacing="sm"
|
<TableTh style={{ width: '50%' }}>Nama Program</TableTh>
|
||||||
>
|
<TableTh style={{ width: '30%' }}>Kategori</TableTh>
|
||||||
<TableThead>
|
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
||||||
<TableTr>
|
</TableTr>
|
||||||
<TableTh style={{ width: '50%' }}>Nama Program</TableTh>
|
</TableThead>
|
||||||
<TableTh style={{ width: '30%' }}>Kategori</TableTh>
|
<TableTbody>
|
||||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
{filteredData.length > 0 ? (
|
||||||
</TableTr>
|
filteredData.map((item) => (
|
||||||
</TableThead>
|
<TableTr key={item.id}>
|
||||||
<TableTbody>
|
<TableTd style={{ width: '50%' }}>
|
||||||
{filteredData.length > 0 ? (
|
<Text fw={500} lineClamp={1}>
|
||||||
filteredData.map((item) => (
|
{item.name || '-'}
|
||||||
<TableTr key={item.id}>
|
</Text>
|
||||||
<TableTd style={{ width: '50%' }}>
|
</TableTd>
|
||||||
<Text fw={500} lineClamp={1}>
|
<TableTd style={{ width: '30%' }}>
|
||||||
{item.name || '-'}
|
<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>
|
</Text>
|
||||||
</TableTd>
|
</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>
|
||||||
))
|
)}
|
||||||
) : (
|
</TableTbody>
|
||||||
<TableTr>
|
</Table>
|
||||||
<TableTd colSpan={3}>
|
</Box>
|
||||||
<Text ta="center" c="dimmed">
|
|
||||||
Tidak ditemukan data dengan kata kunci pencarian
|
|
||||||
</Text>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
)}
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { IconChartBar, IconUsers } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@@ -46,36 +46,44 @@ function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) {
|
|||||||
Indeks Kepuasan Masyarakat
|
Indeks Kepuasan Masyarakat
|
||||||
</Title>
|
</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
radius="xl"
|
color={colors["blue-button"]}
|
||||||
color={colors['blue-button']}
|
|
||||||
variant="pills"
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
|
radius="lg"
|
||||||
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "#F3F4FB",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
}}
|
borderRadius: "1rem",
|
||||||
>
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
{tabs.map((e, i) => (
|
display: "flex",
|
||||||
<Tooltip key={i} label={e.description} withArrow position="bottom" transitionProps={{ transition: 'pop', duration: 200 }}>
|
flexWrap: "nowrap",
|
||||||
<TabsTab
|
gap: "0.5rem",
|
||||||
value={e.value}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
leftSection={e.icon}
|
}}
|
||||||
style={{
|
>
|
||||||
fontWeight: 500,
|
{tabs.map((e, i) => (
|
||||||
fontSize: "0.9rem",
|
<Tooltip key={i} label={e.description} withArrow position="bottom" transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
transition: "all 0.2s ease",
|
<TabsTab
|
||||||
}}
|
value={e.value}
|
||||||
>
|
leftSection={e.icon}
|
||||||
{e.label}
|
style={{
|
||||||
</TabsTab>
|
fontWeight: 500,
|
||||||
</Tooltip>
|
fontSize: "0.9rem",
|
||||||
))}
|
transition: "all 0.2s ease",
|
||||||
</TabsList>
|
}}
|
||||||
|
>
|
||||||
|
{e.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
{tabs.map((e, i) => (
|
{tabs.map((e, i) => (
|
||||||
<TabsPanel key={i} value={e.value} mt="md">
|
<TabsPanel key={i} value={e.value} mt="md">
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import {
|
||||||
|
Stack,
|
||||||
|
Tabs,
|
||||||
|
TabsList,
|
||||||
|
TabsPanel,
|
||||||
|
TabsTab,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
ScrollArea,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconBulb, IconUsers, IconBrandFacebook } from '@tabler/icons-react';
|
import { IconBulb, IconUsers, IconBrandFacebook } from '@tabler/icons-react';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
|
|
||||||
@@ -34,11 +43,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentTab = tabs.find(tab => tab.href === pathname);
|
const currentTab = tabs.find((tab) => tab.href === pathname);
|
||||||
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
|
const [activeTab, setActiveTab] = useState<string | null>(
|
||||||
|
currentTab?.value || tabs[0].value
|
||||||
|
);
|
||||||
|
|
||||||
const handleTabChange = (value: string | null) => {
|
const handleTabChange = (value: string | null) => {
|
||||||
const tab = tabs.find(t => t.value === value);
|
const tab = tabs.find((t) => t.value === value);
|
||||||
if (tab) {
|
if (tab) {
|
||||||
router.push(tab.href);
|
router.push(tab.href);
|
||||||
}
|
}
|
||||||
@@ -46,7 +57,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const match = tabs.find(tab => tab.href === pathname);
|
const match = tabs.find((tab) => tab.href === pathname);
|
||||||
if (match) {
|
if (match) {
|
||||||
setActiveTab(match.value);
|
setActiveTab(match.value);
|
||||||
}
|
}
|
||||||
@@ -57,38 +68,53 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
|
||||||
Profil Desa
|
Profil Desa
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant='pills'
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
}}
|
borderRadius: "1rem",
|
||||||
>
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
{tabs.map((tab, i) => (
|
display: "flex",
|
||||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
flexWrap: "nowrap",
|
||||||
<TabsTab
|
gap: "0.5rem",
|
||||||
value={tab.value}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
leftSection={tab.icon}
|
}}
|
||||||
style={{
|
>
|
||||||
fontWeight: 600,
|
{tabs.map((tab, i) => (
|
||||||
fontSize: "0.9rem",
|
<Tooltip
|
||||||
transition: "all 0.2s ease",
|
key={i}
|
||||||
}}
|
label={tab.tooltip}
|
||||||
|
position="bottom"
|
||||||
|
withArrow
|
||||||
|
transitionProps={{ transition: "pop", duration: 200 }}
|
||||||
>
|
>
|
||||||
{tab.label}
|
<TabsTab
|
||||||
</TabsTab>
|
value={tab.value}
|
||||||
</Tooltip>
|
leftSection={tab.icon}
|
||||||
))}
|
style={{
|
||||||
</TabsList>
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function ListSdgsDesa({ search }: { search: string }) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table>
|
<Table highlightOnHover striped verticalSpacing="sm">
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '60%' }}>Nama Sdgs Desa</TableTh>
|
<TableTh style={{ width: '60%' }}>Nama Sdgs Desa</TableTh>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { IconBuildingCommunity, IconHierarchy2, IconUsers } from '@tabler/icons-react';
|
import { IconBuildingCommunity, IconHierarchy2, IconUsers } from '@tabler/icons-react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@@ -58,21 +58,27 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
Struktur PPID
|
Struktur PPID
|
||||||
</Title>
|
</Title>
|
||||||
<Tabs
|
<Tabs
|
||||||
color={colors['blue-button']}
|
color={colors["blue-button"]}
|
||||||
variant='pills'
|
variant="pills"
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
p="sm"
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
style={{
|
<TabsList
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
p="sm"
|
||||||
borderRadius: "1rem",
|
style={{
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
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) => (
|
{tabs.map((tab, i) => (
|
||||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
||||||
<TabsTab
|
<TabsTab
|
||||||
@@ -89,6 +95,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export default function ListImage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
size="compact-xs"
|
size="compact-xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
stateListImage.del({ name: v.name }).finally(() => {
|
stateListImage.del({ id: v.id }).finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -224,8 +224,8 @@ export const navBar = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "Ekonomi_3",
|
id: "Ekonomi_3",
|
||||||
name: "Struktur Organisasi dan SK Pengurus BUMDesa",
|
name: "Struktur Organisasi Dan Sk Pengurus Bumdesa",
|
||||||
path: "/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi"
|
path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "Ekonomi_4",
|
id: "Ekonomi_4",
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export default async function penghargaanFindMany(context: Context) {
|
|||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
|
include: {
|
||||||
|
image: true,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
prisma.penghargaan.count({ where }),
|
prisma.penghargaan.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,132 +1,71 @@
|
|||||||
// import prisma from "@/lib/prisma";
|
|
||||||
// import { Prisma } from "@prisma/client";
|
|
||||||
// import { Context } from "elysia";
|
|
||||||
|
|
||||||
// type FormCreate = Prisma.DetailDataPengangguranGetPayload<{
|
|
||||||
// select: {
|
|
||||||
// month: true;
|
|
||||||
// year: true;
|
|
||||||
// totalUnemployment: true;
|
|
||||||
// educatedUnemployment: true;
|
|
||||||
// uneducatedUnemployment: true;
|
|
||||||
// percentageChange: true;
|
|
||||||
// };
|
|
||||||
// }>;
|
|
||||||
|
|
||||||
// export default async function detailDataPengangguranCreate(context: Context) {
|
|
||||||
// const body = context.body as FormCreate;
|
|
||||||
|
|
||||||
// // Cek apakah data untuk bulan & tahun tersebut sudah ada
|
|
||||||
// const existing = await prisma.detailDataPengangguran.findFirst({
|
|
||||||
// where: {
|
|
||||||
// month: body.month,
|
|
||||||
// year: body.year,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (existing) {
|
|
||||||
// return {
|
|
||||||
// success: false,
|
|
||||||
// message: `Data bulan ${body.month} ${body.year} sudah ada.`,
|
|
||||||
// data: null,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const created = await prisma.detailDataPengangguran.create({
|
|
||||||
// data: {
|
|
||||||
// month: body.month,
|
|
||||||
// year: body.year,
|
|
||||||
// totalUnemployment: body.totalUnemployment,
|
|
||||||
// educatedUnemployment: body.educatedUnemployment,
|
|
||||||
// uneducatedUnemployment: body.uneducatedUnemployment,
|
|
||||||
// percentageChange: body.percentageChange ?? null,
|
|
||||||
// },
|
|
||||||
// select: {
|
|
||||||
// id: true,
|
|
||||||
// month: true,
|
|
||||||
// year: true,
|
|
||||||
// totalUnemployment: true,
|
|
||||||
// educatedUnemployment: true,
|
|
||||||
// uneducatedUnemployment: true,
|
|
||||||
// percentageChange: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// success: true,
|
|
||||||
// message: "Berhasil menambahkan data pengangguran bulanan",
|
|
||||||
// data: created,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
type FormCreate = Prisma.DetailDataPengangguranGetPayload<{
|
// Define month order for calculation
|
||||||
select: {
|
|
||||||
month: true;
|
|
||||||
year: true;
|
|
||||||
totalUnemployment: true;
|
|
||||||
educatedUnemployment: true;
|
|
||||||
uneducatedUnemployment: true;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const monthOrder = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"];
|
const monthOrder = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"];
|
||||||
|
|
||||||
export default async function detailDataPengangguranCreate(context: Context) {
|
type FormCreate = {
|
||||||
|
month: string;
|
||||||
|
year: number;
|
||||||
|
totalUnemployment: number;
|
||||||
|
educatedUnemployment: number;
|
||||||
|
uneducatedUnemployment: number;
|
||||||
|
percentageChange?: number; // Make it optional since we'll calculate it
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function createDetailDataPengangguran(context: Context) {
|
||||||
const body = context.body as FormCreate;
|
const body = context.body as FormCreate;
|
||||||
|
const yearNumber = parseInt(body.year.toString(), 10);
|
||||||
|
|
||||||
const existing = await prisma.detailDataPengangguran.findFirst({
|
if (isNaN(yearNumber)) {
|
||||||
where: {
|
throw new Error("Year harus berupa angka");
|
||||||
month: body.month,
|
|
||||||
year: body.year,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `Data bulan ${body.month} ${body.year} sudah ada.`,
|
|
||||||
data: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cari bulan sebelumnya
|
// Find current month's index
|
||||||
const currentMonthIndex = monthOrder.indexOf(body.month);
|
const currentMonthIndex = monthOrder.indexOf(body.month);
|
||||||
const prevMonth = currentMonthIndex > 0 ? monthOrder[currentMonthIndex - 1] : "Des";
|
if (currentMonthIndex === -1) {
|
||||||
|
throw new Error("Bulan tidak valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate previous month and year
|
||||||
|
let prevMonthIndex = currentMonthIndex - 1;
|
||||||
|
let prevYear = yearNumber;
|
||||||
|
|
||||||
// Handle year as number for calculations
|
if (prevMonthIndex < 0) {
|
||||||
const currentYear = typeof body.year === 'number' ? body.year : new Date(body.year).getFullYear();
|
prevMonthIndex = 11; // December of previous year
|
||||||
const prevYear = currentMonthIndex > 0 ? currentYear : currentYear - 1;
|
prevYear--; // Decrement year
|
||||||
|
}
|
||||||
|
|
||||||
// Create date objects for Prisma
|
const prevMonth = monthOrder[prevMonthIndex];
|
||||||
const currentYearDate = new Date(currentYear, 0, 1);
|
|
||||||
const prevYearDate = new Date(prevYear, 0, 1);
|
// Find previous month's data
|
||||||
|
const prevMonthData = await prisma.detailDataPengangguran.findFirst({
|
||||||
const prevData = await prisma.detailDataPengangguran.findFirst({
|
|
||||||
where: {
|
where: {
|
||||||
month: prevMonth,
|
month: prevMonth,
|
||||||
year: prevYearDate,
|
year: prevYear,
|
||||||
},
|
},
|
||||||
|
orderBy: { year: 'desc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
let percentageChange: number | null = null;
|
// Calculate percentage change if previous data exists
|
||||||
if (prevData) {
|
let percentageChange = 0;
|
||||||
const change = ((Number(body.totalUnemployment) - Number(prevData.totalUnemployment)) / Number(prevData.totalUnemployment)) * 100;
|
if (prevMonthData && prevMonthData.totalUnemployment > 0) {
|
||||||
percentageChange = parseFloat(change.toFixed(1));
|
const change = ((body.totalUnemployment - prevMonthData.totalUnemployment) /
|
||||||
|
prevMonthData.totalUnemployment) * 100;
|
||||||
|
percentageChange = parseFloat(change.toFixed(1)); // Round to 1 decimal place
|
||||||
|
} else if (body.percentageChange !== undefined) {
|
||||||
|
// Fallback to provided value if no previous data
|
||||||
|
percentageChange = Number(body.percentageChange) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the new record with properly typed values
|
|
||||||
const created = await prisma.detailDataPengangguran.create({
|
const created = await prisma.detailDataPengangguran.create({
|
||||||
data: {
|
data: {
|
||||||
month: body.month,
|
month: body.month,
|
||||||
year: currentYearDate,
|
year: yearNumber,
|
||||||
totalUnemployment: Number(body.totalUnemployment),
|
totalUnemployment: Number(body.totalUnemployment),
|
||||||
educatedUnemployment: Number(body.educatedUnemployment),
|
educatedUnemployment: Number(body.educatedUnemployment),
|
||||||
uneducatedUnemployment: Number(body.uneducatedUnemployment),
|
uneducatedUnemployment: Number(body.uneducatedUnemployment),
|
||||||
percentageChange,
|
percentageChange, // This is now calculated based on previous month's data
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -136,12 +75,9 @@ export default async function detailDataPengangguranCreate(context: Context) {
|
|||||||
educatedUnemployment: true,
|
educatedUnemployment: true,
|
||||||
uneducatedUnemployment: true,
|
uneducatedUnemployment: true,
|
||||||
percentageChange: true,
|
percentageChange: true,
|
||||||
|
createdAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return created;
|
||||||
success: true,
|
|
||||||
message: "Berhasil menambahkan data pengangguran bulanan",
|
|
||||||
data: created,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default async function findByMonthYear(context: Context) {
|
|||||||
const data = await prisma.detailDataPengangguran.findFirst({
|
const data = await prisma.detailDataPengangguran.findFirst({
|
||||||
where: {
|
where: {
|
||||||
month,
|
month,
|
||||||
year: new Date(Number(year), 0, 1), // Convert year number to Date object for Prisma
|
year: Number(year),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,49 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function detailDataPengangguranFindMany() {
|
export default async function detailDataPengangguranFindMany(context: Context) {
|
||||||
const res = await prisma.detailDataPengangguran.findMany({
|
const page = Number(context.query.page) || 1;
|
||||||
orderBy: [{ year: "desc" }, { month: "asc" }],
|
const limit = Number(context.query.limit) || 10;
|
||||||
});
|
const search = (context.query.search as string) || '';
|
||||||
return {
|
const skip = (page - 1) * limit;
|
||||||
data: res,
|
|
||||||
};
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ year: { contains: search, mode: "insensitive" } },
|
||||||
|
{ month: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
prisma.detailDataPengangguran.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: [{ year: "desc" }, { month: "asc" }],
|
||||||
|
}),
|
||||||
|
prisma.detailDataPengangguran.count({
|
||||||
|
where,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success fetch apb desa with pagination",
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch apb desa with pagination",
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ const DetailDataPengangguran = new Elysia({
|
|||||||
prefix: "/detaildatapengangguran",
|
prefix: "/detaildatapengangguran",
|
||||||
tags: ["Ekonomi/Jumlah Pengangguran/Detail Data Pengangguran"],
|
tags: ["Ekonomi/Jumlah Pengangguran/Detail Data Pengangguran"],
|
||||||
})
|
})
|
||||||
.get("/:id", async (context) => {
|
|
||||||
const response = await detailDataPengangguranFindUnique(
|
|
||||||
new Request(context.request)
|
|
||||||
);
|
|
||||||
return response;
|
|
||||||
})
|
|
||||||
.get("/find-many", detailDataPengangguranFindMany)
|
.get("/find-many", detailDataPengangguranFindMany)
|
||||||
|
.get("/month/:month/year/:year", findByMonthYear, {
|
||||||
|
params: t.Object({
|
||||||
|
month: t.String(),
|
||||||
|
year: t.Number(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
.post("/create", detailDataPengangguranCreate, {
|
.post("/create", detailDataPengangguranCreate, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
month: t.String(),
|
month: t.String(),
|
||||||
@@ -24,7 +24,7 @@ const DetailDataPengangguran = new Elysia({
|
|||||||
totalUnemployment: t.Number(),
|
totalUnemployment: t.Number(),
|
||||||
educatedUnemployment: t.Number(),
|
educatedUnemployment: t.Number(),
|
||||||
uneducatedUnemployment: t.Number(),
|
uneducatedUnemployment: t.Number(),
|
||||||
percentageChange: t.Number(),
|
percentageChange: t.Optional(t.Number()),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.put("/:id", detailDataPengangguranUpdate, {
|
.put("/:id", detailDataPengangguranUpdate, {
|
||||||
@@ -37,19 +37,19 @@ const DetailDataPengangguran = new Elysia({
|
|||||||
totalUnemployment: t.Number(),
|
totalUnemployment: t.Number(),
|
||||||
educatedUnemployment: t.Number(),
|
educatedUnemployment: t.Number(),
|
||||||
uneducatedUnemployment: t.Number(),
|
uneducatedUnemployment: t.Number(),
|
||||||
percentageChange: t.Number(),
|
percentageChange: t.Optional(t.Number()),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.delete("/del/:id", detailDataPengangguranDelete, {
|
.delete("/:id", detailDataPengangguranDelete, {
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
id: t.String(),
|
id: t.String(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.get("/month/:month/year/:year", findByMonthYear, {
|
.get("/:id", async (context) => {
|
||||||
params: t.Object({
|
const response = await detailDataPengangguranFindUnique(
|
||||||
month: t.String(),
|
new Request(context.request)
|
||||||
year: t.String(),
|
);
|
||||||
}),
|
return response;
|
||||||
})
|
});
|
||||||
|
|
||||||
export default DetailDataPengangguran;
|
export default DetailDataPengangguran;
|
||||||
|
|||||||
@@ -1,151 +1,97 @@
|
|||||||
// import prisma from "@/lib/prisma";
|
|
||||||
// import { Context } from "elysia";
|
|
||||||
|
|
||||||
// export default async function detailDataPengangguranUpdate(context: Context) {
|
|
||||||
// const id = context.params?.id;
|
|
||||||
|
|
||||||
// if (!id) {
|
|
||||||
// return {
|
|
||||||
// success: false,
|
|
||||||
// message: "ID tidak ditemukan",
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const { month, year, totalUnemployment, educatedUnemployment, uneducatedUnemployment, percentageChange } = context.body as {
|
|
||||||
// month: string;
|
|
||||||
// year: number;
|
|
||||||
// totalUnemployment: number;
|
|
||||||
// educatedUnemployment: number;
|
|
||||||
// uneducatedUnemployment: number;
|
|
||||||
// percentageChange: number;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const duplicate = await prisma.detailDataPengangguran.findFirst({
|
|
||||||
// where: {
|
|
||||||
// month,
|
|
||||||
// year,
|
|
||||||
// NOT: { id },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (duplicate) {
|
|
||||||
// return {
|
|
||||||
// success: false,
|
|
||||||
// message: `Data bulan ${month} ${year} sudah ada.`,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const existing = await prisma.detailDataPengangguran.findUnique({
|
|
||||||
// where: {
|
|
||||||
// id: id,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
// if (!existing) {
|
|
||||||
// return {
|
|
||||||
// success: false,
|
|
||||||
// message: "Data tidak ditemukan",
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const updated = await prisma.detailDataPengangguran.update({
|
|
||||||
// where: { id },
|
|
||||||
// data: {
|
|
||||||
// month,
|
|
||||||
// year,
|
|
||||||
// totalUnemployment,
|
|
||||||
// educatedUnemployment,
|
|
||||||
// uneducatedUnemployment,
|
|
||||||
// percentageChange,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// success: true,
|
|
||||||
// message: "Data berhasil diupdate",
|
|
||||||
// data: updated,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
|
type FormUpdate = {
|
||||||
|
month: string;
|
||||||
|
year: number;
|
||||||
|
totalUnemployment: number;
|
||||||
|
educatedUnemployment: number;
|
||||||
|
uneducatedUnemployment: number;
|
||||||
|
percentageChange?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const monthOrder = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"];
|
const monthOrder = [
|
||||||
|
"Jan", "Feb", "Mar", "Apr", "Mei", "Jun",
|
||||||
|
"Jul", "Agu", "Sep", "Okt", "Nov", "Des"
|
||||||
|
];
|
||||||
|
|
||||||
export default async function detailDataPengangguranUpdate(context: Context) {
|
export default async function detailDataPengangguranUpdate(context: Context) {
|
||||||
const id = context.params?.id;
|
const id = context.params?.id as string;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return { success: false, message: "ID tidak ditemukan" };
|
return { success: false, message: "ID tidak ditemukan" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { month, year: yearInput, totalUnemployment, educatedUnemployment, uneducatedUnemployment } = context.body as {
|
const body = context.body as FormUpdate;
|
||||||
month: string;
|
const {
|
||||||
year: number | string | Date;
|
month,
|
||||||
totalUnemployment: number | string;
|
year: yearInput,
|
||||||
educatedUnemployment: number | string;
|
totalUnemployment,
|
||||||
uneducatedUnemployment: number | string;
|
educatedUnemployment,
|
||||||
};
|
uneducatedUnemployment,
|
||||||
|
} = body;
|
||||||
// Normalize year to Date object
|
|
||||||
const year = typeof yearInput === 'number' ? new Date(yearInput, 0, 1) :
|
// 🔍 Cek duplikat (kecuali current id)
|
||||||
yearInput instanceof Date ? yearInput :
|
const duplicate = await prisma.detailDataPengangguran.findFirst({
|
||||||
new Date(parseInt(yearInput as string), 0, 1);
|
where: {
|
||||||
|
month,
|
||||||
const duplicate = await prisma.detailDataPengangguran.findFirst({
|
year: Number(yearInput),
|
||||||
where: {
|
NOT: { id },
|
||||||
month,
|
},
|
||||||
year,
|
});
|
||||||
NOT: { id },
|
|
||||||
},
|
if (duplicate) {
|
||||||
});
|
|
||||||
|
|
||||||
if (duplicate) {
|
|
||||||
return { success: false, message: `Data bulan ${month} ${year} sudah ada.` };
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = await prisma.detailDataPengangguran.findUnique({ where: { id } });
|
|
||||||
|
|
||||||
if (!current) {
|
|
||||||
return { success: false, message: "Data tidak ditemukan" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentMonthIndex = monthOrder.indexOf(month);
|
|
||||||
const prevMonth = currentMonthIndex > 0 ? monthOrder[currentMonthIndex - 1] : "Des";
|
|
||||||
const currentYear = year.getFullYear();
|
|
||||||
const prevYear = currentMonthIndex > 0 ? currentYear : currentYear - 1;
|
|
||||||
|
|
||||||
const prevData = await prisma.detailDataPengangguran.findFirst({
|
|
||||||
where: {
|
|
||||||
month: prevMonth,
|
|
||||||
year: new Date(prevYear, 0, 1),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let percentageChange: number | null = null;
|
|
||||||
if (prevData) {
|
|
||||||
const change = ((Number(totalUnemployment) - Number(prevData.totalUnemployment)) / Number(prevData.totalUnemployment)) * 100;
|
|
||||||
percentageChange = parseFloat(change.toFixed(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await prisma.detailDataPengangguran.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
month,
|
|
||||||
year: new Date(year), // Ensure it's a new Date instance
|
|
||||||
totalUnemployment: Number(totalUnemployment),
|
|
||||||
educatedUnemployment: Number(educatedUnemployment),
|
|
||||||
uneducatedUnemployment: Number(uneducatedUnemployment),
|
|
||||||
percentageChange,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
message: "Data berhasil diupdate",
|
message: `Data bulan ${month} ${yearInput} sudah ada.`,
|
||||||
data: updated,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔍 Cek data sekarang
|
||||||
|
const current = await prisma.detailDataPengangguran.findUnique({ where: { id } });
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
return { success: false, message: "Data tidak ditemukan" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔢 Cari data bulan sebelumnya
|
||||||
|
const currentMonthIndex = monthOrder.indexOf(month);
|
||||||
|
const prevMonth = currentMonthIndex > 0 ? monthOrder[currentMonthIndex - 1] : "Des";
|
||||||
|
const prevYear = currentMonthIndex > 0 ? yearInput : yearInput - 1;
|
||||||
|
|
||||||
|
const prevData = await prisma.detailDataPengangguran.findFirst({
|
||||||
|
where: {
|
||||||
|
month: prevMonth,
|
||||||
|
year: prevYear,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 📊 Hitung percentageChange
|
||||||
|
let percentageChange: number | null = null;
|
||||||
|
if (prevData && prevData.totalUnemployment > 0) {
|
||||||
|
const change =
|
||||||
|
((Number(totalUnemployment) - Number(prevData.totalUnemployment)) /
|
||||||
|
Number(prevData.totalUnemployment)) *
|
||||||
|
100;
|
||||||
|
percentageChange = parseFloat(change.toFixed(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✏️ Update data
|
||||||
|
const updated = await prisma.detailDataPengangguran.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
month,
|
||||||
|
year: yearInput,
|
||||||
|
totalUnemployment: Number(totalUnemployment),
|
||||||
|
educatedUnemployment: Number(educatedUnemployment),
|
||||||
|
uneducatedUnemployment: Number(uneducatedUnemployment),
|
||||||
|
percentageChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Data berhasil diupdate",
|
||||||
|
data: updated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,43 +1,55 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function apbDesaFindMany(context: Context) {
|
export default async function apbDesaFindMany(context: Context) {
|
||||||
const page = Number(context.query.page) || 1;
|
const page = Number(context.query.page) || 1;
|
||||||
const limit = Number(context.query.limit) || 10;
|
const limit = Number(context.query.limit) || 10;
|
||||||
const skip = (page - 1) * limit;
|
const search = (context.query.search as string) || '';
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
try {
|
const where: any = { isActive: true };
|
||||||
const [data, total] = await Promise.all([
|
|
||||||
prisma.apbDesa.findMany({
|
|
||||||
where: { isActive: true },
|
|
||||||
include: {
|
|
||||||
pendapatan: true,
|
|
||||||
belanja: true,
|
|
||||||
pembiayaan: true,
|
|
||||||
},
|
|
||||||
skip,
|
|
||||||
take: limit,
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
}),
|
|
||||||
prisma.apbDesa.count({
|
|
||||||
where: { isActive: true }
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
// Tambahkan pencarian (jika ada)
|
||||||
success: true,
|
if (search) {
|
||||||
message: "Success fetch apb desa with pagination",
|
where.OR = [
|
||||||
data,
|
{ name: { contains: search, mode: "insensitive" } },
|
||||||
page,
|
{ jumlah: { contains: search, mode: "insensitive" } },
|
||||||
totalPages: Math.ceil(total / limit),
|
];
|
||||||
total,
|
}
|
||||||
};
|
|
||||||
} catch (e) {
|
try {
|
||||||
console.error(e);
|
const [data, total] = await Promise.all([
|
||||||
return {
|
prisma.apbDesa.findMany({
|
||||||
success: false,
|
where,
|
||||||
message: "Failed to fetch apb desa with pagination",
|
include: {
|
||||||
data: null,
|
pendapatan: true,
|
||||||
};
|
belanja: true,
|
||||||
}
|
pembiayaan: true,
|
||||||
}
|
},
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
}),
|
||||||
|
prisma.apbDesa.count({
|
||||||
|
where,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Success fetch apb desa with pagination",
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch apb desa with pagination",
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function belanjaFindMany(context: Context) {
|
export default async function belanjaFindMany(context: Context) {
|
||||||
const page = Number(context.query.page) || 1;
|
const page = Number(context.query.page) || 1;
|
||||||
const limit = Number(context.query.limit) || 10;
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || '';
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ name: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
prisma.belanja.findMany({
|
prisma.belanja.findMany({
|
||||||
where: { isActive: true },
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
}),
|
}),
|
||||||
prisma.belanja.count({
|
prisma.belanja.count({
|
||||||
where: { isActive: true },
|
where,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function pembiayaanFindMany(context: Context) {
|
export default async function pembiayaanFindMany(context: Context) {
|
||||||
const page = Number(context.query.page) || 1;
|
const page = Number(context.query.page) || 1;
|
||||||
const limit = Number(context.query.limit) || 10;
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || '';
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ name: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
prisma.pembiayaan.findMany({
|
prisma.pembiayaan.findMany({
|
||||||
where: { isActive: true },
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
}),
|
}),
|
||||||
prisma.pembiayaan.count({
|
prisma.pembiayaan.count({
|
||||||
where: { isActive: true }
|
where
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
@@ -5,24 +6,34 @@ import { Context } from "elysia";
|
|||||||
async function pendapatanAsliFindMany(context: Context) {
|
async function pendapatanAsliFindMany(context: Context) {
|
||||||
const page = Number(context.query.page) || 1;
|
const page = Number(context.query.page) || 1;
|
||||||
const limit = Number(context.query.limit) || 10;
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || '';
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ name: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
prisma.pendapatan.findMany({
|
prisma.pendapatan.findMany({
|
||||||
where: { isActive: true },
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
}),
|
}),
|
||||||
prisma.pendapatan.count({
|
prisma.pendapatan.count({
|
||||||
where: { isActive: true }
|
where
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Success fetch berita with pagination",
|
message: "Success fetch pendapatan asli desa with pagination",
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
totalPages: Math.ceil(total / limit),
|
totalPages: Math.ceil(total / limit),
|
||||||
|
|||||||
@@ -1,17 +1,56 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function posisiOrganisasiFindMany() {
|
export default async function posisiOrganisasiFindMany(context: Context) {
|
||||||
const data = await prisma.posisiOrganisasi.findMany();
|
// Ambil parameter dari query
|
||||||
|
const page = Number(context.query.page) || 1;
|
||||||
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || "";
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Buat where clause
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ nama: { contains: search, mode: "insensitive" } },
|
||||||
|
{ deskripsi: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ambil data dan total count secara paralel
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
prisma.posisiOrganisasi.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: "asc" },
|
||||||
|
}),
|
||||||
|
prisma.posisiOrganisasi.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil mengambil semua data posisi organisasi",
|
message: "Berhasil mengambil semua data posisi organisasi",
|
||||||
data: data.map((item: any) => ({
|
data: data.map((item: any) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
nama: item.nama,
|
nama: item.nama,
|
||||||
deskripsi: item.deskripsi,
|
deskripsi: item.deskripsi,
|
||||||
hierarki: item.hierarki,
|
hierarki: item.hierarki,
|
||||||
})),
|
})),
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
};
|
};
|
||||||
}
|
} catch (e) {
|
||||||
|
console.error("Error di findMany paginated:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil data posisi organisasi",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,14 +55,18 @@ const fileStorageCreate = async (context: Context) => {
|
|||||||
finalName = desktopName;
|
finalName = desktopName;
|
||||||
finalMimeType = "image/webp";
|
finalMimeType = "image/webp";
|
||||||
} else {
|
} else {
|
||||||
// Kompres dokumen (opsional pakai gzip)
|
// Jika file adalah PDF, simpan tanpa kompresi
|
||||||
const gzBuffer = zlib.gzipSync(buffer);
|
if (file.type === "application/pdf") {
|
||||||
const docName = `${finalName}.gz`;
|
await fs.writeFile(path.join(rootPath, finalName), buffer);
|
||||||
|
}
|
||||||
await fs.writeFile(path.join(rootPath, docName), gzBuffer);
|
// Jika file lain, kompres dengan gzip
|
||||||
|
else {
|
||||||
finalName = docName;
|
const gzBuffer = zlib.gzipSync(buffer);
|
||||||
finalMimeType = "application/gzip";
|
const compressedName = `${finalName}.gz`;
|
||||||
|
await fs.writeFile(path.join(rootPath, compressedName), gzBuffer);
|
||||||
|
finalName = compressedName;
|
||||||
|
finalMimeType = "application/gzip";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await prisma.fileStorage.create({
|
const data = await prisma.fileStorage.create({
|
||||||
|
|||||||
@@ -1,20 +1,59 @@
|
|||||||
import prisma from "@/lib/prisma"
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// /api/berita/findManyPaginated.ts
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
const laporanPublikFindMany = async () => {
|
async function laporanPublikFindMany(context: Context) {
|
||||||
const laporanPublik = await prisma.laporanPublik.findMany({
|
// Ambil parameter dari query
|
||||||
|
const page = Number(context.query.page) || 1;
|
||||||
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || "";
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
// Buat where clause
|
||||||
|
const where: any = { isActive: true };
|
||||||
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ judul: { contains: search, mode: "insensitive" } },
|
||||||
|
{ lokasi: { contains: search, mode: "insensitive" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ambil data dan total count secara paralel
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
prisma.laporanPublik.findMany({
|
||||||
|
where,
|
||||||
include: {
|
include: {
|
||||||
penanganan: true
|
penanganan: true,
|
||||||
},
|
},
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
tanggalWaktu: 'desc'
|
tanggalWaktu: "desc",
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
|
prisma.laporanPublik.count({ where }),
|
||||||
return {
|
]);
|
||||||
success: true,
|
|
||||||
data: laporanPublik,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Berhasil ambil laporan publik dengan pagination",
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error di findMany paginated:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Gagal mengambil data laporan publik",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default laporanPublikFindMany;
|
export default laporanPublikFindMany;
|
||||||
|
|
||||||
157
src/app/coba/page.tsx
Normal file
157
src/app/coba/page.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Avatar, Box, Flex, Group, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
|
||||||
|
import { IconAmbulance, IconBadge, IconBuildingBank, IconBuildingSkyscraper, IconFiretruck, IconFirstAidKit, IconPhoneCall, IconUser } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
|
||||||
|
const emergencyServices = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Polisi",
|
||||||
|
icon: <IconBadge size={24} color={colors['blue-button']} />,
|
||||||
|
nomor: '081xxxxxxxxx'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Ambulans",
|
||||||
|
icon: <IconAmbulance size={24} color={colors['blue-button']} />,
|
||||||
|
nomor: '081xxxxxxxxx'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Pemadam Kebakaran",
|
||||||
|
icon: <IconFiretruck size={24} color={colors['blue-button']} />,
|
||||||
|
nomor: '081xxxxxxxxx'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const healthFacilities = [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "Puskesmas Darmasaba",
|
||||||
|
icon: <IconFirstAidKit size={24} color={colors['blue-button']} />,
|
||||||
|
nomor: '081xxxxxxxxx'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "UGD Terdekat",
|
||||||
|
icon: <IconAmbulance size={24} color={colors['blue-button']} />,
|
||||||
|
nomor: '081xxxxxxxxx'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const villageGovernment = [
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: "Kantor Desa Darmasaba",
|
||||||
|
icon: <IconBuildingSkyscraper size={24} color={colors['blue-button']} />,
|
||||||
|
nomor: '081xxxxxxxxx'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: "Kepala Desa",
|
||||||
|
icon: <IconUser size={24} color={colors['blue-button']} />,
|
||||||
|
nomor: '081xxxxxxxxx'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
function Page() {
|
||||||
|
return (
|
||||||
|
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||||
|
<Box>
|
||||||
|
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||||
|
Kontak Darurat
|
||||||
|
</Text>
|
||||||
|
<Text px={{ base: 20, md: 150 }} ta={"center"} fz={{ base: "h4", md: "h3" }} >
|
||||||
|
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
|
<Stack gap={'lg'}>
|
||||||
|
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
|
||||||
|
<Flex justify={'center'} gap={'lg'} align={'center'}>
|
||||||
|
<Avatar radius={"xl"} size={'lg'} bg={colors['BG-trans']}>
|
||||||
|
<IconPhoneCall size={30} color={colors["blue-button"]} />
|
||||||
|
</Avatar>
|
||||||
|
<Box>
|
||||||
|
<Text ta={'center'} c={colors['blue-button']} py={10} fz={{ base: "md", md: "h4" }} fw={"bold"} >
|
||||||
|
Nomor Darurat Utama
|
||||||
|
</Text>
|
||||||
|
<Text ta={'center'} fw={"bold"} fz={'h2'} c={colors["blue-button"]}>112</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
|
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl">
|
||||||
|
{/* Layanan Darurat */}
|
||||||
|
<Paper p="lg" radius="md" bg={colors['white-trans-1']}>
|
||||||
|
<Group pb="md" align="center">
|
||||||
|
<Avatar radius={"xl"} size={'lg'} bg={colors['BG-trans']}>
|
||||||
|
<IconFirstAidKit size={30} color={colors["blue-button"]} />
|
||||||
|
</Avatar>
|
||||||
|
<Text fw="bold" fz={{ base: "lg", md: "xl" }} c={colors["blue-button"]}>
|
||||||
|
Layanan Darurat
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{emergencyServices.map((v, k) => (
|
||||||
|
<Group key={k} px="md" py="sm" bg={colors['BG-trans']} mb="xs" justify="space-between" style={{ borderRadius: 8 }}>
|
||||||
|
<Group>
|
||||||
|
{v.icon}
|
||||||
|
<Text>{v.name}</Text>
|
||||||
|
</Group>
|
||||||
|
<Text>{v.nomor}</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Fasilitas Kesehatan */}
|
||||||
|
<Paper p="lg" radius="md" bg={colors['white-trans-1']}>
|
||||||
|
<Group pb="md" align="center">
|
||||||
|
<Avatar radius={"xl"} size={'lg'} bg={colors['BG-trans']}>
|
||||||
|
<IconBuildingSkyscraper size={30} color={colors["blue-button"]} />
|
||||||
|
</Avatar>
|
||||||
|
<Text fw="bold" fz={{ base: "lg", md: "xl" }} c={colors["blue-button"]}>
|
||||||
|
Fasilitas Kesehatan
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{healthFacilities.map((v, k) => (
|
||||||
|
<Group key={k} px="md" py="sm" bg={colors['BG-trans']} mb="xs" justify="space-between" style={{ borderRadius: 8 }}>
|
||||||
|
<Group>
|
||||||
|
{v.icon}
|
||||||
|
<Text>{v.name}</Text>
|
||||||
|
</Group>
|
||||||
|
<Text>{v.nomor}</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Pemerintah Desa */}
|
||||||
|
<Paper p="lg" radius="md" bg={colors['white-trans-1']}>
|
||||||
|
<Group pb="md" align="center">
|
||||||
|
<Avatar radius={"xl"} size={'lg'} bg={colors['BG-trans']}>
|
||||||
|
<IconBuildingBank size={30} color={colors["blue-button"]} />
|
||||||
|
</Avatar>
|
||||||
|
<Text fw="bold" fz={{ base: "lg", md: "xl" }} c={colors["blue-button"]}>
|
||||||
|
Pemerintah Desa
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{villageGovernment.map((v, k) => (
|
||||||
|
<Group key={k} px="md" py="sm" bg={colors['BG-trans']} mb="xs" justify="space-between" style={{ borderRadius: 8 }}>
|
||||||
|
<Group>
|
||||||
|
{v.icon}
|
||||||
|
<Text>{v.name}</Text>
|
||||||
|
</Group>
|
||||||
|
<Text>{v.nomor}</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Paper>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -76,7 +76,7 @@ function Page() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<SimpleGrid py={48} cols={{ base: 1, sm: 2, lg: 3 }} spacing="2xl">
|
<SimpleGrid py={48} cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
Array.from({ length: 6 }).map((_, i) => (
|
Array.from({ length: 6 }).map((_, i) => (
|
||||||
<Skeleton key={i} h={360} radius="xl" />
|
<Skeleton key={i} h={360} radius="xl" />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Center, Paper, SimpleGrid, Box, Text } from '@mantine/core';
|
import { Box, Center, Paper } from '@mantine/core';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
function ProfileDesa() {
|
function ProfileDesa() {
|
||||||
return (
|
return (
|
||||||
@@ -12,35 +11,6 @@ function ProfileDesa() {
|
|||||||
</Center>
|
</Center>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Center>
|
</Center>
|
||||||
<Center>
|
|
||||||
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
|
|
||||||
<SimpleGrid
|
|
||||||
cols={{
|
|
||||||
base: 1,
|
|
||||||
sm: 3
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
<Paper p={"sm"} w={{ base: "100%", md: "100%" }}>
|
|
||||||
<Text fz={50} c={colors['blue-button']} ta={"center"} fw={"bold"}>8</Text>
|
|
||||||
<Text fz={{ base: "md", md: "h3"}} ta={"center"}>Tempat Wisata</Text>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Paper p={"sm"} w={{ base: "100%", md: "100%" }}>
|
|
||||||
<Text fz={50} c={colors['blue-button']} ta={"center"} fw={"bold"}>8</Text>
|
|
||||||
<Text fz={{ base: "md", md: "h3"}} ta={"center"}>Tempat Wisata</Text>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Paper p={"sm"} w={{ base: "100%", md: "100%" }}>
|
|
||||||
<Text fz={50} c={colors['blue-button']} ta={"center"} fw={"bold"}>8</Text>
|
|
||||||
<Text fz={{ base: "md", md: "h3"}} ta={"center"}>Tempat Wisata</Text>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
</SimpleGrid>
|
|
||||||
</Paper>
|
|
||||||
</Center>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function SemuaPerbekel() {
|
|||||||
{data.map((v: any, k: number) => (
|
{data.map((v: any, k: number) => (
|
||||||
<Paper
|
<Paper
|
||||||
key={k}
|
key={k}
|
||||||
radius="2xl"
|
radius="xl"
|
||||||
shadow="md"
|
shadow="md"
|
||||||
withBorder
|
withBorder
|
||||||
p="lg"
|
p="lg"
|
||||||
@@ -65,7 +65,7 @@ function SemuaPerbekel() {
|
|||||||
className="hover:shadow-xl hover:scale-[1.02]"
|
className="hover:shadow-xl hover:scale-[1.02]"
|
||||||
>
|
>
|
||||||
<Stack gap="md" align="center">
|
<Stack gap="md" align="center">
|
||||||
<Box w="100%" h={300} style={{ overflow: "hidden", borderRadius: "1rem" }}>
|
<Box w="100%" h={500} style={{ overflow: "hidden", borderRadius: "1rem" }}>
|
||||||
<Image
|
<Image
|
||||||
src={v.image?.link}
|
src={v.image?.link}
|
||||||
alt={v.nama || "Foto Perbekel"}
|
alt={v.nama || "Foto Perbekel"}
|
||||||
|
|||||||
@@ -19,8 +19,14 @@ type DataPengangguran = {
|
|||||||
function Page() {
|
function Page() {
|
||||||
const [chartData, setChartData] = useState<DataPengangguran[]>([])
|
const [chartData, setChartData] = useState<DataPengangguran[]>([])
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const [yearlyData, setYearlyData] = useState<{
|
||||||
|
total: number;
|
||||||
|
educated: number;
|
||||||
|
uneducated: number;
|
||||||
|
year: number;
|
||||||
|
} | null>(null);
|
||||||
const state = useProxy(jumlahPengangguranState.jumlahPengangguran)
|
const state = useProxy(jumlahPengangguranState.jumlahPengangguran)
|
||||||
// const [selectedYear, setSelectedYear] = useState<string | null>(null);
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
@@ -30,14 +36,36 @@ function Page() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
if (state.findMany.data) {
|
if (state.findMany.data) {
|
||||||
|
// Set chart data
|
||||||
setChartData(state.findMany.data.map((item) => ({
|
setChartData(state.findMany.data.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
bulan: item.month,
|
bulan: item.month,
|
||||||
berpendidikan: Number(item.educatedUnemployment),
|
berpendidikan: Number(item.educatedUnemployment),
|
||||||
takberpendidikan: Number(item.uneducatedUnemployment),
|
takberpendidikan: Number(item.uneducatedUnemployment),
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
// Calculate yearly totals
|
||||||
|
const currentYearData = state.findMany.data.filter(item => item.year === currentYear);
|
||||||
|
if (currentYearData.length > 0) {
|
||||||
|
const yearlyTotal = {
|
||||||
|
total: 0,
|
||||||
|
educated: 0,
|
||||||
|
uneducated: 0,
|
||||||
|
year: currentYear
|
||||||
|
};
|
||||||
|
|
||||||
|
currentYearData.forEach(item => {
|
||||||
|
yearlyTotal.total += Number(item.totalUnemployment) || 0;
|
||||||
|
yearlyTotal.educated += Number(item.educatedUnemployment) || 0;
|
||||||
|
yearlyTotal.uneducated += Number(item.uneducatedUnemployment) || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
setYearlyData(yearlyTotal);
|
||||||
|
} else {
|
||||||
|
setYearlyData(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [state.findMany.data]);
|
}, [state.findMany.data, currentYear]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||||
@@ -65,10 +93,10 @@ function Page() {
|
|||||||
<IconUserOff size={35} color={colors['blue-button']} />
|
<IconUserOff size={35} color={colors['blue-button']} />
|
||||||
<Text fz="h4" fw={600}>Total Pengangguran</Text>
|
<Text fz="h4" fw={600}>Total Pengangguran</Text>
|
||||||
<Text fz="h2" fw={700} c={colors['blue-button']}>
|
<Text fz="h2" fw={700} c={colors['blue-button']}>
|
||||||
{state.findMany.data?.[0]?.totalUnemployment || 0} Orang
|
{yearlyData?.total.toLocaleString() || 0} Orang
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz="sm" c="dimmed">
|
||||||
Data tahun {state.findMany.data?.[0]?.year ? new Date(state.findMany.data[0].year).getFullYear() : ''}
|
Total data tahun {currentYear}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -79,12 +107,14 @@ function Page() {
|
|||||||
<IconSchool size={35} color="#5082EE" />
|
<IconSchool size={35} color="#5082EE" />
|
||||||
<Text fz="h4" fw={600}>Pengangguran Terdidik</Text>
|
<Text fz="h4" fw={600}>Pengangguran Terdidik</Text>
|
||||||
<Text fz="h2" fw={700} c="#5082EE">
|
<Text fz="h2" fw={700} c="#5082EE">
|
||||||
{state.findMany.data?.[0]?.educatedUnemployment || 0} Orang
|
{yearlyData?.educated.toLocaleString() || 0} Orang
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz="sm" c="dimmed">
|
||||||
{state.findMany.data?.[0]?.totalUnemployment ?
|
{yearlyData ?
|
||||||
`${Math.round((state.findMany.data[0].educatedUnemployment / state.findMany.data[0].totalUnemployment) * 100)}% dari total` :
|
<>
|
||||||
'0% dari total'}
|
{((yearlyData.educated / yearlyData.total) * 100).toFixed(1)}%
|
||||||
|
dari total pengangguran
|
||||||
|
</> : 'Tidak ada data'}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -95,12 +125,14 @@ function Page() {
|
|||||||
<IconSchoolOff size={35} color="#DA524C" />
|
<IconSchoolOff size={35} color="#DA524C" />
|
||||||
<Text fz="h4" fw={600}>Pengangguran Tidak Terdidik</Text>
|
<Text fz="h4" fw={600}>Pengangguran Tidak Terdidik</Text>
|
||||||
<Text fz="h2" fw={700} c="#DA524C">
|
<Text fz="h2" fw={700} c="#DA524C">
|
||||||
{state.findMany.data?.[0]?.uneducatedUnemployment || 0} Orang
|
{yearlyData?.uneducated.toLocaleString() || 0} Orang
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz="sm" c="dimmed">
|
||||||
{state.findMany.data?.[0]?.totalUnemployment ?
|
{yearlyData ?
|
||||||
`${Math.round((state.findMany.data[0].uneducatedUnemployment / state.findMany.data[0].totalUnemployment) * 100)}% dari total` :
|
<>
|
||||||
'0% dari total'}
|
{((yearlyData.uneducated / yearlyData.total) * 100).toFixed(1)}%
|
||||||
|
dari total pengangguran
|
||||||
|
</> : 'Tidak ada data'}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function Page() {
|
|||||||
const tarif = (data?.tarifdanlayanan as TarifDanLayanan) || null;
|
const tarif = (data?.tarifdanlayanan as TarifDanLayanan) || null;
|
||||||
const kontak = (data?.kontak as Kontak) || {
|
const kontak = (data?.kontak as Kontak) || {
|
||||||
telepon: '(0361) 123456',
|
telepon: '(0361) 123456',
|
||||||
whatsapp: '081234567890',
|
whatsapp: '6289647037426',
|
||||||
email: 'info@fasilitas-kesehatan.id'
|
email: 'info@fasilitas-kesehatan.id'
|
||||||
};
|
};
|
||||||
const lokasi = (data?.lokasi as Lokasi) || {
|
const lokasi = (data?.lokasi as Lokasi) || {
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ import { IconSearch, IconFileInfo, IconMail, IconBrandWhatsapp } from '@tabler/i
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const listData = useProxy(daftarInformasiPublik)
|
const listData = useProxy(daftarInformasiPublik)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -39,8 +41,8 @@ function Page() {
|
|||||||
} = listData.findMany
|
} = listData.findMany
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 5, search)
|
load(page, 5, debouncedSearch)
|
||||||
}, [page, search])
|
}, [page, debouncedSearch])
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import profileLandingPageState from "@/app/admin/(dashboard)/_state/landing-page/profile";
|
import profileLandingPageState from "@/app/admin/(dashboard)/_state/landing-page/profile";
|
||||||
import { Box, Center, Image, Paper, SimpleGrid, Stack, Text, Tooltip } from "@mantine/core";
|
import {
|
||||||
|
Box,
|
||||||
|
Center,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
SimpleGrid,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
Skeleton,
|
||||||
|
useMantineColorScheme,
|
||||||
|
ScrollArea,
|
||||||
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useTransitionRouter } from "next-view-transitions";
|
import { useTransitionRouter } from "next-view-transitions";
|
||||||
@@ -11,29 +25,36 @@ type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: tr
|
|||||||
|
|
||||||
function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
||||||
const router = useTransitionRouter();
|
const router = useTransitionRouter();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const isDark = colorScheme === "dark";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div whileHover={{ scale: 1.04 }}>
|
<motion.div whileHover={{ scale: 1.03 }}>
|
||||||
<Tooltip label={`Lihat ${data.name}`} withArrow>
|
<Tooltip label={`Lihat ${data.name}`} withArrow>
|
||||||
<Paper
|
<Paper
|
||||||
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
|
onClick={() => router.push(`/darmasaba/program-inovasi/${data.id}`)}
|
||||||
p="xl"
|
p="lg"
|
||||||
radius="2xl"
|
radius="xl"
|
||||||
bg="white"
|
shadow="sm"
|
||||||
className="cursor-pointer transition-all shadow-md hover:shadow-xl"
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className="cursor-pointer transition-all"
|
||||||
|
bg={isDark ? "dark.6" : "white"}
|
||||||
>
|
>
|
||||||
<Center h={180}>
|
<Center h={160}>
|
||||||
{data.image?.link ? (
|
{data.image?.link ? (
|
||||||
<Image
|
<Image
|
||||||
src={data.image.link}
|
src={data.image.link}
|
||||||
alt={data.name}
|
alt={data.name}
|
||||||
fit="contain"
|
radius="md"
|
||||||
radius="lg"
|
fit="cover"
|
||||||
|
h={140}
|
||||||
|
w="100%"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{ objectFit: "contain", objectPosition: "center" }}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Stack align="center" gap="xs">
|
<Stack align="center" gap="xs">
|
||||||
<IconPhotoOff size={40} stroke={1.5} />
|
<IconPhotoOff size={38} stroke={1.5} />
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Belum ada gambar
|
Belum ada gambar
|
||||||
</Text>
|
</Text>
|
||||||
@@ -41,7 +62,7 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
|||||||
)}
|
)}
|
||||||
</Center>
|
</Center>
|
||||||
<Box mt="md">
|
<Box mt="md">
|
||||||
<Text fw={600} ta="center" size="lg" c="black">
|
<Text fw={600} ta="center" size="md">
|
||||||
{data.name}
|
{data.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -58,12 +79,22 @@ function ModuleView() {
|
|||||||
listImageState.findMany.load();
|
listImageState.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!listImageState.findMany.loading && !listImageState.findMany.data?.length) {
|
if (listImageState.findMany.loading) {
|
||||||
|
return (
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg">
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} height={220} radius="xl" />
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!listImageState.findMany.data?.length) {
|
||||||
return (
|
return (
|
||||||
<Center h={320}>
|
<Center h={320}>
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<IconPhotoOff size={54} stroke={1.5} />
|
<IconPhotoOff size={54} stroke={1.5} />
|
||||||
<Text size="lg" fw={600} c="white">
|
<Text size="lg" fw={600}>
|
||||||
Belum ada program inovasi
|
Belum ada program inovasi
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
@@ -75,11 +106,20 @@ function ModuleView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
|
<ScrollArea h={280} // ✅ tinggi fixed, bisa disesuaikan
|
||||||
|
scrollbarSize={8}
|
||||||
|
offsetScrollbars
|
||||||
|
type="auto"
|
||||||
|
styles={{
|
||||||
|
viewport: { paddingRight: 8 }, // kasih jarak biar scroll nggak dempet
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg" mt="lg">
|
||||||
{listImageState.findMany.data?.map((item) => (
|
{listImageState.findMany.data?.map((item) => (
|
||||||
<ModuleItem key={item.id} data={item} />
|
<ModuleItem key={item.id} data={item} />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const getCurrentTime = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isWorkingHours = (currentTime: string): boolean => {
|
const isWorkingHours = (currentTime: string): boolean => {
|
||||||
const [openTime, closeTime] = ["08:00", "16:00"];
|
const [openTime, closeTime] = ["08:00", "17:00"];
|
||||||
const compareTimes = (time1: string, time2: string) => {
|
const compareTimes = (time1: string, time2: string) => {
|
||||||
const [hour1, minute1] = time1.split(":").map(Number);
|
const [hour1, minute1] = time1.split(":").map(Number);
|
||||||
const [hour2, minute2] = time2.split(":").map(Number);
|
const [hour2, minute2] = time2.split(":").map(Number);
|
||||||
@@ -54,8 +54,8 @@ const getWorkStatus = (day: string, currentTime: string): { status: string; mess
|
|||||||
}
|
}
|
||||||
const isOpen = isWorkingHours(currentTime);
|
const isOpen = isWorkingHours(currentTime);
|
||||||
return isOpen
|
return isOpen
|
||||||
? { status: "Buka", message: "08:00 - 16:00" }
|
? { status: "Buka", message: "08:00 - 17:00" }
|
||||||
: { status: "Tutup", message: "08:00 - 16:00" };
|
: { status: "Tutup", message: "08:00 - 17:00" };
|
||||||
};
|
};
|
||||||
|
|
||||||
function LandingPage() {
|
function LandingPage() {
|
||||||
|
|||||||
@@ -1,37 +1,87 @@
|
|||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
|
|
||||||
|
interface FileStorageItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
link: string;
|
||||||
|
realName: string;
|
||||||
|
mimeType: string;
|
||||||
|
category: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
deletedAt: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
data: FileStorageItem[];
|
||||||
|
meta: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
total: number;
|
||||||
|
realName: string;
|
||||||
|
}
|
||||||
|
|
||||||
const stateListImage = proxy<{
|
const stateListImage = proxy<{
|
||||||
list: { name: string; url: string; total: number }[] | null;
|
list: ListItem[] | null;
|
||||||
page: number;
|
page: number;
|
||||||
count: number;
|
count: number;
|
||||||
total: number | undefined;
|
total: number | undefined;
|
||||||
load: (params?: { search?: string }) => Promise<void>;
|
load: (params?: { search?: string; page?: number }) => Promise<void>;
|
||||||
del: ({ name }: { name: string }) => Promise<void>;
|
del: (params: { id: string }) => Promise<void>;
|
||||||
}>({
|
}>({
|
||||||
list: null,
|
list: null,
|
||||||
page: 1,
|
page: 1,
|
||||||
count: 20,
|
count: 10,
|
||||||
total: undefined,
|
total: undefined,
|
||||||
async load(params?: { search?: string }) {
|
|
||||||
const { search = "" } = params ?? {};
|
async load(params?: { search?: string; page?: number }) {
|
||||||
const { data } = await ApiFetch.api.imgs.get({
|
const { search = "", page = this.page } = params ?? {};
|
||||||
query: {
|
this.page = page;
|
||||||
page: this.page,
|
|
||||||
count: this.count,
|
try {
|
||||||
search,
|
const response = await ApiFetch.api.fileStorage["findMany"].get({
|
||||||
},
|
query: {
|
||||||
});
|
page: this.page,
|
||||||
this.list = data;
|
search,
|
||||||
if (data?.[0]?.total) {
|
},
|
||||||
this.total = Math.ceil(data[0].total / this.count);
|
}) as { data: ApiResponse };
|
||||||
} else {
|
|
||||||
this.total = undefined;
|
if (response?.data?.data) {
|
||||||
|
this.list = response.data.data.map((file) => ({
|
||||||
|
id: file.id,
|
||||||
|
name: file.name,
|
||||||
|
url: file.link || `/api/fileStorage/${file.realName}`,
|
||||||
|
total: response.data.meta?.total || 0,
|
||||||
|
realName: file.realName,
|
||||||
|
}));
|
||||||
|
this.total = response.data.meta?.totalPages;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading images:", error);
|
||||||
|
this.list = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async del({ name }: { name: string }) {
|
|
||||||
await ApiFetch.api.img({ name }).delete();
|
async del({ id }: { id: string }) {
|
||||||
this.load();
|
try {
|
||||||
|
await ApiFetch.api.fileStorage.delete({ id });
|
||||||
|
await this.load({ page: this.page });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting image:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user