Compare commits
27 Commits
nico/test-
...
nico/17-de
| Author | SHA1 | Date | |
|---|---|---|---|
| dc8793e3ae | |||
| c8484357cb | |||
| 342e9bbc65 | |||
| f6f77d9e35 | |||
| a00481152c | |||
| 242ea86f77 | |||
| 99c2c9c6d7 | |||
| ac2fc1a705 | |||
| 9dbe172165 | |||
| cc318d4d54 | |||
| dcb8017594 | |||
| ec3ad12531 | |||
| dad44c0537 | |||
| 867dce42f0 | |||
| 7bb17ddf22 | |||
| a4069d3cba | |||
| ffe5e6dd9f | |||
| dcf195f54f | |||
| c03a6b3aed | |||
| 1bb9f239db | |||
| a213ff7d37 | |||
| 0018bdc251 | |||
| 83fb39a957 | |||
| 7238692dd0 | |||
| 8b50139d79 | |||
| 066180fc0e | |||
| 67f29aabef |
@@ -1,14 +1,15 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
'postcss-preset-mantine': {},
|
'postcss-preset-mantine': {},
|
||||||
'postcss-simple-vars': {
|
'postcss-simple-vars': {
|
||||||
variables: {
|
variables: {
|
||||||
'mantine-breakpoint-xs': '36em',
|
/* Mobile first */
|
||||||
'mantine-breakpoint-sm': '48em',
|
'mantine-breakpoint-xs': '30em', // 480px → mobile kecil–normal
|
||||||
'mantine-breakpoint-md': '62em',
|
'mantine-breakpoint-sm': '48em', // 768px → tablet / mobile landscape
|
||||||
'mantine-breakpoint-lg': '75em',
|
'mantine-breakpoint-md': '64em', // 1024px → laptop & desktop kecil
|
||||||
'mantine-breakpoint-xl': '88em',
|
'mantine-breakpoint-lg': '80em', // 1280px → desktop standar
|
||||||
},
|
'mantine-breakpoint-xl': '90em', // 1440px+ → desktop besar
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ model MediaSosial {
|
|||||||
name String
|
name String
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
|
icon String?
|
||||||
iconUrl String? @db.VarChar(255)
|
iconUrl String? @db.VarChar(255)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -782,24 +783,22 @@ model Penghargaan {
|
|||||||
|
|
||||||
// ========================================= FASILITAS KESEHATAN ========================================= //
|
// ========================================= FASILITAS KESEHATAN ========================================= //
|
||||||
model FasilitasKesehatan {
|
model FasilitasKesehatan {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
informasiumum InformasiUmum @relation(fields: [informasiUmumId], references: [id])
|
informasiumum InformasiUmum @relation(fields: [informasiUmumId], references: [id])
|
||||||
informasiUmumId String
|
informasiUmumId String
|
||||||
layananunggulan LayananUnggulan @relation(fields: [layananUnggulanId], references: [id])
|
layananunggulan LayananUnggulan @relation(fields: [layananUnggulanId], references: [id])
|
||||||
layananUnggulanId String
|
layananUnggulanId String
|
||||||
dokterdantenagamedis DokterdanTenagaMedis @relation(fields: [dokterdanTenagaMedisId], references: [id])
|
dokterdantenagamedis DokterdanTenagaMedis[] @relation("Dokter")
|
||||||
dokterdanTenagaMedisId String
|
fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id])
|
||||||
fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id])
|
fasilitasPendukungId String
|
||||||
fasilitasPendukungId String
|
prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id])
|
||||||
prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id])
|
prosedurPendaftaranId String
|
||||||
prosedurPendaftaranId String
|
tarifdanlayanan TarifDanLayanan[] @relation("Tarif")
|
||||||
tarifdanlayanan TarifDanLayanan @relation(fields: [tarifDanLayananId], references: [id])
|
|
||||||
tarifDanLayananId String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model InformasiUmum {
|
model InformasiUmum {
|
||||||
@@ -825,15 +824,20 @@ model LayananUnggulan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model DokterdanTenagaMedis {
|
model DokterdanTenagaMedis {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
specialist String
|
specialist String
|
||||||
jadwal String
|
jadwal String
|
||||||
createdAt DateTime @default(now())
|
jadwalLibur String?
|
||||||
updatedAt DateTime @updatedAt
|
jamBukaOperasional String?
|
||||||
deletedAt DateTime @default(now())
|
jamTutupOperasional String?
|
||||||
isActive Boolean @default(true)
|
jamBukaLibur String?
|
||||||
FasilitasKesehatan FasilitasKesehatan[]
|
jamTutupLibur String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime @default(now())
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
FasilitasKesehatan FasilitasKesehatan[] @relation("Dokter")
|
||||||
}
|
}
|
||||||
|
|
||||||
model FasilitasPendukung {
|
model FasilitasPendukung {
|
||||||
@@ -864,7 +868,7 @@ model TarifDanLayanan {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
FasilitasKesehatan FasilitasKesehatan[]
|
FasilitasKesehatan FasilitasKesehatan[] @relation("Tarif")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================= JADWAL KEGIATAN ========================================= //
|
// ========================================= JADWAL KEGIATAN ========================================= //
|
||||||
|
|||||||
BIN
public/mangupuraaward.jpeg
Normal file
BIN
public/mangupuraaward.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
76
src/app/admin/(dashboard)/_com/selectSocialMedia.tsx
Normal file
76
src/app/admin/(dashboard)/_com/selectSocialMedia.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Box, Image, Select, rem } from '@mantine/core';
|
||||||
|
|
||||||
|
const sosmedMap = {
|
||||||
|
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
||||||
|
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
||||||
|
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
||||||
|
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
||||||
|
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
||||||
|
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
||||||
|
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
||||||
|
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
||||||
|
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
||||||
|
custom: { label: 'Custom Icon', src: null },
|
||||||
|
};
|
||||||
|
|
||||||
|
type SosmedKey = keyof typeof sosmedMap;
|
||||||
|
|
||||||
|
const sosmedList = Object.entries(sosmedMap).map(([value, item]) => ({
|
||||||
|
value,
|
||||||
|
label: item.label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function SelectSosialMedia({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: SosmedKey;
|
||||||
|
onChange: (value: SosmedKey) => void;
|
||||||
|
}) {
|
||||||
|
const selected = value;
|
||||||
|
const selectedImage = sosmedMap[selected]?.src;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box maw={300}>
|
||||||
|
<Select
|
||||||
|
placeholder="Pilih sosial media"
|
||||||
|
value={selected}
|
||||||
|
data={sosmedList}
|
||||||
|
searchable={false}
|
||||||
|
withCheckIcon={false}
|
||||||
|
onChange={(val) => val && onChange(val as SosmedKey)}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
textAlign: 'left',
|
||||||
|
fontSize: rem(16),
|
||||||
|
paddingLeft: 36,
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
left: 10,
|
||||||
|
right: 'auto',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 🔥 PREVIEW DIPISAH DI LUAR SELECT */}
|
||||||
|
{selectedImage && (
|
||||||
|
<Box mt="md">
|
||||||
|
<Image
|
||||||
|
alt=""
|
||||||
|
src={selectedImage}
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #eee',
|
||||||
|
padding: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
src/app/admin/(dashboard)/_com/selectSocialMediaEdit.tsx
Normal file
56
src/app/admin/(dashboard)/_com/selectSocialMediaEdit.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Box, Select } from '@mantine/core';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const sosmedMap = {
|
||||||
|
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
||||||
|
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
||||||
|
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
||||||
|
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
||||||
|
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
||||||
|
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
||||||
|
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
||||||
|
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
||||||
|
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
||||||
|
custom: { label: 'Custom Icon', src: null },
|
||||||
|
};
|
||||||
|
|
||||||
|
type SosmedKey = keyof typeof sosmedMap;
|
||||||
|
|
||||||
|
const sosmedList = Object.entries(sosmedMap).map(([value, item]) => ({
|
||||||
|
value,
|
||||||
|
label: item.label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function SelectSocialMediaEdit({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (val: SosmedKey) => void;
|
||||||
|
}) {
|
||||||
|
const [selected, setSelected] = useState<SosmedKey>('facebook');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value && sosmedMap[value as SosmedKey]) {
|
||||||
|
setSelected(value as SosmedKey);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Select
|
||||||
|
label="Jenis Media Sosial"
|
||||||
|
value={selected}
|
||||||
|
data={sosmedList}
|
||||||
|
searchable={false}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (!val) return;
|
||||||
|
setSelected(val as SosmedKey);
|
||||||
|
onChange(val as SosmedKey);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,29 +9,30 @@ import { z } from "zod";
|
|||||||
// Validasi form
|
// Validasi form
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(1, "Nama harus diisi"),
|
name: z.string().min(1, "Nama harus diisi"),
|
||||||
|
|
||||||
informasiUmum: z.object({
|
informasiUmum: z.object({
|
||||||
fasilitas: z.string().min(1, "Fasilitas harus diisi"),
|
fasilitas: z.string().min(1),
|
||||||
alamat: z.string().min(1, "Alamat harus diisi"),
|
alamat: z.string().min(1),
|
||||||
jamOperasional: z.string().min(1, "Jam operasional harus diisi"),
|
jamOperasional: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
layananUnggulan: z.object({
|
layananUnggulan: z.object({
|
||||||
content: z.string().min(1, "Layanan unggulan harus diisi"),
|
content: z.string().min(1),
|
||||||
}),
|
|
||||||
dokterdanTenagaMedis: z.object({
|
|
||||||
name: z.string().min(1, "Nama dokter harus diisi"),
|
|
||||||
specialist: z.string().min(1, "Spesialis harus diisi"),
|
|
||||||
jadwal: z.string().min(1, "Jadwal harus diisi"),
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// NOW ARRAY OF STRING (ID)
|
||||||
|
dokterdanTenagaMedis: z.array(z.string()).min(1, "Minimal pilih 1 dokter"),
|
||||||
|
|
||||||
fasilitasPendukung: z.object({
|
fasilitasPendukung: z.object({
|
||||||
content: z.string().min(1, "Fasilitas pendukung harus diisi"),
|
content: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
prosedurPendaftaran: z.object({
|
prosedurPendaftaran: z.object({
|
||||||
content: z.string().min(1, "Prosedur pendaftaran harus diisi"),
|
content: z.string().min(1),
|
||||||
}),
|
|
||||||
tarifDanLayanan: z.object({
|
|
||||||
layanan: z.string().min(1, "Layanan harus diisi"),
|
|
||||||
tarif: z.string().min(1, "Tarif harus diisi"),
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// NOW ARRAY OF STRING (ID)
|
||||||
|
tarifDanLayanan: z.array(z.string()).min(1, "Minimal pilih 1 tarif"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Default form kosong
|
// Default form kosong
|
||||||
@@ -45,21 +46,34 @@ const defaultForm = {
|
|||||||
layananUnggulan: {
|
layananUnggulan: {
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
dokterdanTenagaMedis: {
|
|
||||||
name: "",
|
dokterdanTenagaMedis: [] as string[], // ← array kosong
|
||||||
specialist: "",
|
tarifDanLayanan: [] as string[], // ← array kosong
|
||||||
jadwal: "",
|
|
||||||
},
|
|
||||||
fasilitasPendukung: {
|
fasilitasPendukung: {
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
prosedurPendaftaran: {
|
prosedurPendaftaran: {
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
tarifDanLayanan: {
|
};
|
||||||
layanan: "",
|
|
||||||
tarif: "",
|
type DokterItem = {
|
||||||
},
|
id: string;
|
||||||
|
name: string;
|
||||||
|
specialist: string;
|
||||||
|
jadwal: string;
|
||||||
|
jadwalLibur: string;
|
||||||
|
jamBukaOperasional: string;
|
||||||
|
jamTutupOperasional: string;
|
||||||
|
jamBukaLibur: string;
|
||||||
|
jamTutupLibur: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TarifItem = {
|
||||||
|
id: string;
|
||||||
|
layanan: string;
|
||||||
|
tarif: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fasilitasKesehatan = proxy({
|
const fasilitasKesehatan = proxy({
|
||||||
@@ -186,33 +200,26 @@ const fasilitasKesehatan = proxy({
|
|||||||
|
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
fasilitasKesehatan.edit.id = data.id;
|
this.form = {
|
||||||
fasilitasKesehatan.edit.form = {
|
|
||||||
name: data.name,
|
name: data.name,
|
||||||
informasiUmum: {
|
informasiUmum: {
|
||||||
fasilitas: data.informasiumum.fasilitas,
|
fasilitas: data.informasiumum.fasilitas,
|
||||||
alamat: data.informasiumum.alamat,
|
alamat: data.informasiumum.alamat,
|
||||||
jamOperasional: data.informasiumum.jamOperasional,
|
jamOperasional: data.informasiumum.jamOperasional,
|
||||||
},
|
},
|
||||||
layananUnggulan: {
|
|
||||||
content: data.layananunggulan.content,
|
|
||||||
},
|
|
||||||
dokterdanTenagaMedis: {
|
|
||||||
name: data.dokterdantenagamedis.name,
|
|
||||||
specialist: data.dokterdantenagamedis.specialist,
|
|
||||||
jadwal: data.dokterdantenagamedis.jadwal,
|
|
||||||
},
|
|
||||||
fasilitasPendukung: {
|
fasilitasPendukung: {
|
||||||
content: data.fasilitaspendukung.content,
|
content: data.fasilitaspendukung.content,
|
||||||
},
|
},
|
||||||
prosedurPendaftaran: {
|
prosedurPendaftaran: {
|
||||||
content: data.prosedurpendaftaran.content,
|
content: data.prosedurpendaftaran.content,
|
||||||
},
|
},
|
||||||
tarifDanLayanan: {
|
// map relasi -> array of IDs
|
||||||
layanan: data.tarifdanlayanan.layanan,
|
layananUnggulan: {
|
||||||
tarif: data.tarifdanlayanan.tarif,
|
content: data.layananunggulan.content,
|
||||||
},
|
},
|
||||||
|
dokterdanTenagaMedis: data.dokterdantenagamedis?.map((v: DokterItem) => v.id) ?? [],
|
||||||
|
tarifDanLayanan: data.tarifdanlayanan?.map((v: TarifItem) => v.id) ?? [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async submit() {
|
async submit() {
|
||||||
@@ -238,22 +245,15 @@ const fasilitasKesehatan = proxy({
|
|||||||
layananUnggulan: {
|
layananUnggulan: {
|
||||||
content: fasilitasKesehatan.edit.form.layananUnggulan.content,
|
content: fasilitasKesehatan.edit.form.layananUnggulan.content,
|
||||||
},
|
},
|
||||||
dokterdanTenagaMedis: {
|
dokterdanTenagaMedis:
|
||||||
name: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.name,
|
fasilitasKesehatan.edit.form.dokterdanTenagaMedis,
|
||||||
specialist:
|
|
||||||
fasilitasKesehatan.edit.form.dokterdanTenagaMedis.specialist,
|
|
||||||
jadwal: fasilitasKesehatan.edit.form.dokterdanTenagaMedis.jadwal,
|
|
||||||
},
|
|
||||||
fasilitasPendukung: {
|
fasilitasPendukung: {
|
||||||
content: fasilitasKesehatan.edit.form.fasilitasPendukung.content,
|
content: fasilitasKesehatan.edit.form.fasilitasPendukung.content,
|
||||||
},
|
},
|
||||||
prosedurPendaftaran: {
|
prosedurPendaftaran: {
|
||||||
content: fasilitasKesehatan.edit.form.prosedurPendaftaran.content,
|
content: fasilitasKesehatan.edit.form.prosedurPendaftaran.content,
|
||||||
},
|
},
|
||||||
tarifDanLayanan: {
|
tarifDanLayanan: fasilitasKesehatan.edit.form.tarifDanLayanan,
|
||||||
layanan: fasilitasKesehatan.edit.form.tarifDanLayanan.layanan,
|
|
||||||
tarif: fasilitasKesehatan.edit.form.tarifDanLayanan.tarif,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
@@ -320,12 +320,26 @@ const templateDokterForm = z.object({
|
|||||||
name: z.string().min(1, "Nama tidak boleh kosong"),
|
name: z.string().min(1, "Nama tidak boleh kosong"),
|
||||||
specialist: z.string().min(1, "Spesialis tidak boleh kosong"),
|
specialist: z.string().min(1, "Spesialis tidak boleh kosong"),
|
||||||
jadwal: z.string().min(1, "Jadwal tidak boleh kosong"),
|
jadwal: z.string().min(1, "Jadwal tidak boleh kosong"),
|
||||||
|
jadwalLibur: z.string().min(1, "Jadwal libur tidak boleh kosong"),
|
||||||
|
jamBukaOperasional: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Jam buka operasional tidak boleh kosong"),
|
||||||
|
jamTutupOperasional: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Jam tutup operasional tidak boleh kosong"),
|
||||||
|
jamBukaLibur: z.string().min(1, "Jam buka libur tidak boleh kosong"),
|
||||||
|
jamTutupLibur: z.string().min(1, "Jam tutup libur tidak boleh kosong"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultDokterForm = {
|
const defaultDokterForm = {
|
||||||
name: "",
|
name: "",
|
||||||
specialist: "",
|
specialist: "",
|
||||||
jadwal: "",
|
jadwal: "",
|
||||||
|
jadwalLibur: "",
|
||||||
|
jamBukaOperasional: "",
|
||||||
|
jamTutupOperasional: "",
|
||||||
|
jamBukaLibur: "",
|
||||||
|
jamTutupLibur: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const dokter = proxy({
|
const dokter = proxy({
|
||||||
@@ -463,6 +477,11 @@ const dokter = proxy({
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
specialist: data.specialist,
|
specialist: data.specialist,
|
||||||
jadwal: data.jadwal,
|
jadwal: data.jadwal,
|
||||||
|
jadwalLibur: data.jadwalLibur,
|
||||||
|
jamBukaOperasional: data.jamBukaOperasional,
|
||||||
|
jamTutupOperasional: data.jamTutupOperasional,
|
||||||
|
jamBukaLibur: data.jamBukaLibur,
|
||||||
|
jamTutupLibur: data.jamTutupLibur,
|
||||||
};
|
};
|
||||||
return data; // Return the loaded data
|
return data; // Return the loaded data
|
||||||
} else {
|
} else {
|
||||||
@@ -487,6 +506,11 @@ const dokter = proxy({
|
|||||||
name: this.form.name,
|
name: this.form.name,
|
||||||
specialist: this.form.specialist,
|
specialist: this.form.specialist,
|
||||||
jadwal: this.form.jadwal,
|
jadwal: this.form.jadwal,
|
||||||
|
jadwalLibur: this.form.jadwalLibur,
|
||||||
|
jamBukaOperasional: this.form.jamBukaOperasional,
|
||||||
|
jamTutupOperasional: this.form.jamTutupOperasional,
|
||||||
|
jamBukaLibur: this.form.jamBukaLibur,
|
||||||
|
jamTutupLibur: this.form.jamTutupLibur,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cek = templateDokterForm.safeParse(formData);
|
const cek = templateDokterForm.safeParse(formData);
|
||||||
@@ -567,9 +591,255 @@ const dokter = proxy({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const templateTarifForm = z.object({
|
||||||
|
tarif: z.string().min(1, "Tarif tidak boleh kosong"),
|
||||||
|
layanan: z.string().min(1, "Layanan tidak boleh kosong"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultTarifForm = {
|
||||||
|
tarif: "",
|
||||||
|
layanan: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const tarif = proxy({
|
||||||
|
create: {
|
||||||
|
form: defaultTarifForm,
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = templateTarifForm.safeParse(tarif.create.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
tarif.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.kesehatan.tarifdanlayanan["create"].post(
|
||||||
|
tarif.create.form
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
const id = res.data?.data;
|
||||||
|
if (id) {
|
||||||
|
toast.success("Sukses menambahkan");
|
||||||
|
tarif.create.form = { ...defaultTarifForm };
|
||||||
|
tarif.findMany.load();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toast.error("failed create");
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.log((error as Error).message);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
tarif.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.TarifDanLayananGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
tarif.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
|
tarif.findMany.page = page;
|
||||||
|
tarif.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.kesehatan.tarifdanlayanan[
|
||||||
|
"findMany"
|
||||||
|
].get({ query });
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
tarif.findMany.data = res.data.data ?? [];
|
||||||
|
tarif.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
tarif.findMany.data = [];
|
||||||
|
tarif.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch tarif dan layanan paginated:", err);
|
||||||
|
tarif.findMany.data = [];
|
||||||
|
tarif.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
tarif.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.TarifDanLayananGetPayload<{
|
||||||
|
omit: { isActive: true };
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/kesehatan/tarifdanlayanan/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
tarif.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to fetch tarif dan layanan",
|
||||||
|
res.statusText
|
||||||
|
);
|
||||||
|
tarif.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching tarif dan layanan", error);
|
||||||
|
tarif.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...defaultTarifForm },
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/kesehatan/tarifdanlayanan/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result?.success) {
|
||||||
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
|
this.form = {
|
||||||
|
tarif: data.tarif,
|
||||||
|
layanan: data.layanan
|
||||||
|
};
|
||||||
|
return data; // Return the loaded data
|
||||||
|
} else {
|
||||||
|
throw new Error(result?.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading tarif dan layanan:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submit() {
|
||||||
|
const id = this.id;
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
tarif: this.form.tarif,
|
||||||
|
layanan: this.form.layanan
|
||||||
|
};
|
||||||
|
|
||||||
|
const cek = templateTarifForm.safeParse(formData);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v: any) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true;
|
||||||
|
const res = await fetch(`/api/kesehatan/tarifdanlayanan/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok || !result?.success) {
|
||||||
|
throw new Error(result?.message || "Gagal update data");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Berhasil update data!");
|
||||||
|
await tarif.findMany.load();
|
||||||
|
return result.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update error:", error);
|
||||||
|
toast.error("Gagal update data tarif dan layanan");
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
return toast.warn("ID tidak valid");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
tarif.delete.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/kesehatan/tarifdanlayanan/del/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result?.success) {
|
||||||
|
toast.success(
|
||||||
|
result.message || "tarif dan layanan berhasil dihapus"
|
||||||
|
);
|
||||||
|
await tarif.findMany.load(); // refresh list
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
result?.message || "Gagal menghapus tarif dan layanan"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus tarif dan layanan");
|
||||||
|
} finally {
|
||||||
|
tarif.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const fasilitasKesehatanState = proxy({
|
const fasilitasKesehatanState = proxy({
|
||||||
fasilitasKesehatan,
|
fasilitasKesehatan,
|
||||||
dokter,
|
dokter,
|
||||||
|
tarif
|
||||||
});
|
});
|
||||||
|
|
||||||
export default fasilitasKesehatanState;
|
export default fasilitasKesehatanState;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const programInovasi = proxy({
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
link: ""
|
link: "",
|
||||||
} as ProgramInovasiForm,
|
} as ProgramInovasiForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
async create() {
|
||||||
@@ -71,20 +71,21 @@ const programInovasi = proxy({
|
|||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
programInovasi.findMany.loading = true; // Use the full path to access the property
|
// Change to arrow function
|
||||||
|
programInovasi.findMany.loading = true; // Use the full path to access the property
|
||||||
programInovasi.findMany.page = page;
|
programInovasi.findMany.page = page;
|
||||||
programInovasi.findMany.search = search;
|
programInovasi.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
const query: any = { page, limit };
|
const query: any = { page, limit };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.programinovasi[
|
const res = await ApiFetch.api.landingpage.programinovasi[
|
||||||
"findMany"
|
"findMany"
|
||||||
].get({
|
].get({
|
||||||
query
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
programInovasi.findMany.data = res.data.data || [];
|
programInovasi.findMany.data = res.data.data || [];
|
||||||
programInovasi.findMany.total = res.data.total || 0;
|
programInovasi.findMany.total = res.data.total || 0;
|
||||||
@@ -389,7 +390,10 @@ const pejabatDesa = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure ID is properly encoded in the URL
|
// Ensure ID is properly encoded in the URL
|
||||||
const url = new URL(`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`, window.location.origin);
|
const url = new URL(
|
||||||
|
`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`,
|
||||||
|
window.location.origin
|
||||||
|
);
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -438,16 +442,19 @@ const pejabatDesa = proxy({
|
|||||||
|
|
||||||
const templateMediaSosial = z.object({
|
const templateMediaSosial = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
imageId: z.string().nullable().optional(),
|
||||||
iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
|
iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
|
||||||
|
icon: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type MediaSosialForm = {
|
type MediaSosialForm = {
|
||||||
name: string;
|
name: string;
|
||||||
imageId: string;
|
imageId: string | null; // boleh null
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
|
icon: string | null; // boleh null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const mediaSosial = proxy({
|
const mediaSosial = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: {} as MediaSosialForm,
|
form: {} as MediaSosialForm,
|
||||||
@@ -455,9 +462,10 @@ const mediaSosial = proxy({
|
|||||||
async create() {
|
async create() {
|
||||||
// Ensure all required fields are non-null
|
// Ensure all required fields are non-null
|
||||||
const formData = {
|
const formData = {
|
||||||
name: mediaSosial.create.form.name || "",
|
name: mediaSosial.create.form.name ?? "",
|
||||||
imageId: mediaSosial.create.form.imageId || "",
|
imageId: mediaSosial.create.form.imageId ?? null, // FIXED
|
||||||
iconUrl: mediaSosial.create.form.iconUrl || "",
|
iconUrl: mediaSosial.create.form.iconUrl ?? "",
|
||||||
|
icon: mediaSosial.create.form.icon ?? null, // FIXED
|
||||||
};
|
};
|
||||||
|
|
||||||
const cek = templateMediaSosial.safeParse(formData);
|
const cek = templateMediaSosial.safeParse(formData);
|
||||||
@@ -492,20 +500,19 @@ const mediaSosial = proxy({
|
|||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
mediaSosial.findMany.loading = true; // Use the full path to access the property
|
// Change to arrow function
|
||||||
|
mediaSosial.findMany.loading = true; // Use the full path to access the property
|
||||||
mediaSosial.findMany.page = page;
|
mediaSosial.findMany.page = page;
|
||||||
mediaSosial.findMany.search = search;
|
mediaSosial.findMany.search = search;
|
||||||
try {
|
try {
|
||||||
const query: any = { page, limit };
|
const query: any = { page, limit };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.mediasosial[
|
const res = await ApiFetch.api.landingpage.mediasosial["findMany"].get({
|
||||||
"findMany"
|
|
||||||
].get({
|
|
||||||
query,
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
mediaSosial.findMany.data = res.data.data || [];
|
mediaSosial.findMany.data = res.data.data || [];
|
||||||
mediaSosial.findMany.total = res.data.total || 0;
|
mediaSosial.findMany.total = res.data.total || 0;
|
||||||
@@ -537,7 +544,7 @@ const mediaSosial = proxy({
|
|||||||
toast.warn("ID tidak valid");
|
toast.warn("ID tidak valid");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSosial.update.loading = true;
|
mediaSosial.update.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/landingpage/mediasosial/${id}`);
|
const res = await fetch(`/api/landingpage/mediasosial/${id}`);
|
||||||
@@ -586,66 +593,72 @@ const mediaSosial = proxy({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
id: "",
|
id: "",
|
||||||
form: {} as MediaSosialForm,
|
form: {} as MediaSosialForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.warn("ID tidak valid");
|
toast.warn("ID tidak valid");
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/landingpage/mediasosial/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal
|
const result = await response.json();
|
||||||
|
|
||||||
try {
|
if (result?.success) {
|
||||||
const response = await fetch(`/api/landingpage/mediasosial/${id}`, {
|
const data = result.data;
|
||||||
method: "GET",
|
this.id = data.id;
|
||||||
headers: {
|
this.form = {
|
||||||
"Content-Type": "application/json",
|
name: data.name || "",
|
||||||
},
|
imageId: data.imageId || null,
|
||||||
});
|
iconUrl: data.iconUrl || "",
|
||||||
|
icon: data.icon || null,
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
};
|
||||||
}
|
return data;
|
||||||
|
} else {
|
||||||
const result = await response.json();
|
throw new Error(
|
||||||
|
result?.message || "Gagal mengambil data media sosial"
|
||||||
if (result?.success) {
|
);
|
||||||
const data = result.data;
|
|
||||||
this.id = data.id;
|
|
||||||
this.form = {
|
|
||||||
name: data.name || "",
|
|
||||||
imageId: data.imageId || "",
|
|
||||||
iconUrl: data.iconUrl || "",
|
|
||||||
};
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
throw new Error(result?.message || "Gagal mengambil data media sosial");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error((error as Error).message);
|
|
||||||
toast.error("Terjadi kesalahan saat mengambil data media sosial");
|
|
||||||
} finally {
|
|
||||||
mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error
|
|
||||||
}
|
}
|
||||||
},
|
} catch (error) {
|
||||||
|
console.error((error as Error).message);
|
||||||
async update() {
|
toast.error("Terjadi kesalahan saat mengambil data media sosial");
|
||||||
const cek = templateMediaSosial.safeParse(mediaSosial.update.form);
|
} finally {
|
||||||
if (!cek.success) {
|
mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error
|
||||||
const err = `[${cek.error.issues
|
}
|
||||||
.map((v) => `${v.path.join(".")}`)
|
},
|
||||||
.join("\n")}] required`;
|
|
||||||
toast.error(err);
|
async update() {
|
||||||
return false;
|
const cek = templateMediaSosial.safeParse(mediaSosial.update.form);
|
||||||
}
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
try {
|
.map((v) => `${v.path.join(".")}`)
|
||||||
mediaSosial.update.loading = true;
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
const response = await fetch(`/api/landingpage/mediasosial/${this.id}`, {
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mediaSosial.update.loading = true;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/landingpage/mediasosial/${this.id}`,
|
||||||
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -654,38 +667,40 @@ const mediaSosial = proxy({
|
|||||||
name: this.form.name,
|
name: this.form.name,
|
||||||
imageId: this.form.imageId,
|
imageId: this.form.imageId,
|
||||||
iconUrl: this.form.iconUrl,
|
iconUrl: this.form.iconUrl,
|
||||||
|
icon: this.form.icon,
|
||||||
}),
|
}),
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(
|
|
||||||
errorData.message || `HTTP error! status: ${response.status}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
const result = await response.json();
|
|
||||||
|
if (!response.ok) {
|
||||||
if (result.success) {
|
const errorData = await response.json().catch(() => ({}));
|
||||||
toast.success("Berhasil update media sosial");
|
throw new Error(
|
||||||
await mediaSosial.findMany.load(); // refresh list
|
errorData.message || `HTTP error! status: ${response.status}`
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
throw new Error(result.message || "Gagal update media sosial");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating media sosial:", error);
|
|
||||||
toast.error(
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Terjadi kesalahan saat update media sosial"
|
|
||||||
);
|
);
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
mediaSosial.update.loading = false;
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Berhasil update media sosial");
|
||||||
|
await mediaSosial.findMany.load(); // refresh list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update media sosial");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating media sosial:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Terjadi kesalahan saat update media sosial"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
mediaSosial.update.loading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const profileLandingPageState = proxy({
|
const profileLandingPageState = proxy({
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -6,145 +7,207 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
nik: z.string().min(3, "NIK minimal 3 karakter"),
|
nik: z
|
||||||
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
|
.string()
|
||||||
|
.min(3, "NIK minimal 3 karakter")
|
||||||
|
.max(16, "NIK maksimal 16 angka"),
|
||||||
|
notelp: z
|
||||||
|
.string()
|
||||||
|
.min(3, "Nomor Telepon minimal 3 karakter")
|
||||||
|
.max(15, "Nomor Telepon maksimal 15 angka"),
|
||||||
alamat: z.string().min(3, "Alamat minimal 3 karakter"),
|
alamat: z.string().min(3, "Alamat minimal 3 karakter"),
|
||||||
email: z.string().min(3, "Email minimal 3 karakter"),
|
email: z.string().min(3, "Email minimal 3 karakter"),
|
||||||
jenisInformasiDimintaId: z.string().nonempty(),
|
jenisInformasiDimintaId: z.string().nonempty(),
|
||||||
caraMemperolehInformasiId: z.string().nonempty(),
|
caraMemperolehInformasiId: z.string().nonempty(),
|
||||||
caraMemperolehSalinanInformasiId: z.string().nonempty(),
|
caraMemperolehSalinanInformasiId: z.string().nonempty(),
|
||||||
})
|
});
|
||||||
|
|
||||||
const jenisInformasiDiminta = proxy({
|
const jenisInformasiDiminta = proxy({
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| null
|
| null
|
||||||
| Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
|
| Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[],
|
||||||
async load(){
|
async load() {
|
||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi["find-many"].get();
|
const res =
|
||||||
if (res.status === 200) {
|
await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi[
|
||||||
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
|
"find-many"
|
||||||
}
|
].get();
|
||||||
}
|
if (res.status === 200) {
|
||||||
}
|
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
|
||||||
})
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const caraMemperolehInformasi = proxy({
|
const caraMemperolehInformasi = proxy({
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| null
|
| null
|
||||||
| Prisma.CaraMemperolehInformasiGetPayload<{ omit: { isActive: true } }>[],
|
| Prisma.CaraMemperolehInformasiGetPayload<{
|
||||||
async load() {
|
omit: { isActive: true };
|
||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi["find-many"].get();
|
}>[],
|
||||||
if (res.status === 200) {
|
async load() {
|
||||||
caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
|
const res =
|
||||||
}
|
await ApiFetch.api.ppid.permohonaninformasipublik.memperolehInformasi[
|
||||||
}
|
"find-many"
|
||||||
}
|
].get();
|
||||||
})
|
if (res.status === 200) {
|
||||||
|
caraMemperolehInformasi.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const caraMemperolehSalinanInformasi = proxy({
|
const caraMemperolehSalinanInformasi = proxy({
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| null
|
| null
|
||||||
| Prisma.CaraMemperolehSalinanInformasiGetPayload<{ omit: { isActive: true } }>[],
|
| Prisma.CaraMemperolehSalinanInformasiGetPayload<{
|
||||||
async load() {
|
omit: { isActive: true };
|
||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi["find-many"].get();
|
}>[],
|
||||||
if (res.status === 200) {
|
async load() {
|
||||||
caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
|
const res =
|
||||||
}
|
await ApiFetch.api.ppid.permohonaninformasipublik.salinanInformasi[
|
||||||
}
|
"find-many"
|
||||||
}
|
].get();
|
||||||
})
|
if (res.status === 200) {
|
||||||
console.log(caraMemperolehSalinanInformasi)
|
caraMemperolehSalinanInformasi.findMany.data = res.data?.data ?? [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(caraMemperolehSalinanInformasi);
|
||||||
|
|
||||||
type PermohonanInformasiPublikForm = Prisma.PermohonanInformasiPublikGetPayload<{
|
type PermohonanInformasiPublikForm =
|
||||||
|
Prisma.PermohonanInformasiPublikGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
name: true;
|
name: true;
|
||||||
nik: true;
|
nik: true;
|
||||||
notelp: true;
|
notelp: true;
|
||||||
alamat: true;
|
alamat: true;
|
||||||
email: true;
|
email: true;
|
||||||
jenisInformasiDimintaId: true;
|
jenisInformasiDimintaId: true;
|
||||||
caraMemperolehInformasiId: true;
|
caraMemperolehInformasiId: true;
|
||||||
caraMemperolehSalinanInformasiId: true;
|
caraMemperolehSalinanInformasiId: true;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const statepermohonanInformasiPublik = proxy({
|
const statepermohonanInformasiPublik = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: {} as PermohonanInformasiPublikForm,
|
form: {} as PermohonanInformasiPublikForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
async create(){
|
async create() {
|
||||||
const cek = templateForm.safeParse(statepermohonanInformasiPublik.create.form);
|
const cek = templateForm.safeParse(
|
||||||
if(!cek.success) {
|
statepermohonanInformasiPublik.create.form
|
||||||
const err = `[${cek.error.issues
|
);
|
||||||
.map((v) => `${v.path.join(".")}`)
|
|
||||||
.join("\n")}] required`;
|
if (!cek.success) {
|
||||||
return toast.error(err);
|
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
|
||||||
}
|
return false; // ⬅️ tambahkan return false
|
||||||
try {
|
}
|
||||||
statepermohonanInformasiPublik.create.loading = true;
|
|
||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
|
try {
|
||||||
if (res.status === 200) {
|
statepermohonanInformasiPublik.create.loading = true;
|
||||||
statepermohonanInformasiPublik.findMany.load();
|
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
|
||||||
return toast.success("Sukses menambahkan");
|
"create"
|
||||||
}
|
].post(statepermohonanInformasiPublik.create.form);
|
||||||
return toast.error("failed create");
|
|
||||||
} catch (error) {
|
if (res.data?.success === false) {
|
||||||
console.log((error as Error).message);
|
toast.error(res.data?.message);
|
||||||
} finally {
|
return false; // ⬅️ gagal
|
||||||
statepermohonanInformasiPublik.create.loading = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toast.success("Sukses menambahkan");
|
||||||
|
return true; // ⬅️ sukses
|
||||||
|
} catch {
|
||||||
|
toast.error("Terjadi kesalahan server");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
statepermohonanInformasiPublik.create.loading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
findMany: {
|
},
|
||||||
data: null as
|
findMany: {
|
||||||
| Prisma.PermohonanInformasiPublikGetPayload<{ include: {
|
data: null as
|
||||||
caraMemperolehSalinanInformasi: true,
|
| Prisma.PermohonanInformasiPublikGetPayload<{
|
||||||
jenisInformasiDiminta: true,
|
|
||||||
caraMemperolehInformasi: true,
|
|
||||||
} }>[]
|
|
||||||
| null,
|
|
||||||
async load() {
|
|
||||||
const res = await ApiFetch.api.ppid.permohonaninformasipublik["find-many"].get();
|
|
||||||
if (res.status === 200) {
|
|
||||||
statepermohonanInformasiPublik.findMany.data = res.data?.data ?? [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
findUnique: {
|
|
||||||
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
|
|
||||||
include: {
|
include: {
|
||||||
jenisInformasiDiminta: true,
|
caraMemperolehSalinanInformasi: true;
|
||||||
caraMemperolehInformasi: true,
|
jenisInformasiDiminta: true;
|
||||||
caraMemperolehSalinanInformasi: true,
|
caraMemperolehInformasi: true;
|
||||||
};
|
};
|
||||||
}> | null,
|
}>[]
|
||||||
async load(id: string) {
|
| null,
|
||||||
try {
|
page: 1,
|
||||||
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
|
totalPages: 1,
|
||||||
if (res.ok) {
|
total: 0,
|
||||||
const data = await res.json();
|
loading: false,
|
||||||
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
|
search: "",
|
||||||
} else {
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
console.error("Failed to fetch program inovasi:", res.statusText);
|
// Change to arrow function
|
||||||
statepermohonanInformasiPublik.findUnique.data = null;
|
statepermohonanInformasiPublik.findMany.loading = true; // Use the full path to access the property
|
||||||
}
|
statepermohonanInformasiPublik.findMany.page = page;
|
||||||
} catch (error) {
|
statepermohonanInformasiPublik.findMany.search = search;
|
||||||
console.error("Error fetching program inovasi:", error);
|
try {
|
||||||
statepermohonanInformasiPublik.findUnique.data = null;
|
const query: any = { page, limit };
|
||||||
}
|
if (search) query.search = search;
|
||||||
},
|
|
||||||
},
|
const res = await ApiFetch.api.ppid.permohonaninformasipublik[
|
||||||
|
"find-many"
|
||||||
})
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
statepermohonanInformasiPublik.findMany.data = res.data.data || [];
|
||||||
|
statepermohonanInformasiPublik.findMany.total = res.data.total || 0;
|
||||||
|
statepermohonanInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
|
||||||
|
statepermohonanInformasiPublik.findMany.data = [];
|
||||||
|
statepermohonanInformasiPublik.findMany.total = 0;
|
||||||
|
statepermohonanInformasiPublik.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading permohonan keberatan informasi:", error);
|
||||||
|
statepermohonanInformasiPublik.findMany.data = [];
|
||||||
|
statepermohonanInformasiPublik.findMany.total = 0;
|
||||||
|
statepermohonanInformasiPublik.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
statepermohonanInformasiPublik.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.PermohonanInformasiPublikGetPayload<{
|
||||||
|
include: {
|
||||||
|
jenisInformasiDiminta: true;
|
||||||
|
caraMemperolehInformasi: true;
|
||||||
|
caraMemperolehSalinanInformasi: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
statepermohonanInformasiPublik.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch program inovasi:", res.statusText);
|
||||||
|
statepermohonanInformasiPublik.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching program inovasi:", error);
|
||||||
|
statepermohonanInformasiPublik.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const statepermohonanInformasiPublikForm = proxy({
|
const statepermohonanInformasiPublikForm = proxy({
|
||||||
statepermohonanInformasiPublik,
|
statepermohonanInformasiPublik,
|
||||||
jenisInformasiDiminta,
|
jenisInformasiDiminta,
|
||||||
caraMemperolehInformasi,
|
caraMemperolehInformasi,
|
||||||
caraMemperolehSalinanInformasi,
|
caraMemperolehSalinanInformasi,
|
||||||
})
|
});
|
||||||
|
|
||||||
export default statepermohonanInformasiPublikForm;
|
export default statepermohonanInformasiPublikForm;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -5,82 +6,130 @@ import { proxy } from "valtio";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateForm = z.object({
|
const templateForm = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
email: z.string().min(3, "Email minimal 3 karakter"),
|
email: z.string().min(3, "Email minimal 3 karakter"),
|
||||||
notelp: z.string().min(3, "Nomor Telepon minimal 3 karakter"),
|
notelp: z
|
||||||
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
|
.string()
|
||||||
})
|
.min(3, "Nomor Telepon minimal 3 karakter")
|
||||||
|
.max(15, "Nomor Telepon maksimal 15 angka"),
|
||||||
|
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
|
||||||
|
});
|
||||||
|
|
||||||
type PermohonanKeberatanInformasiForm = Prisma.FormulirPermohonanKeberatanGetPayload<{
|
type PermohonanKeberatanInformasiForm =
|
||||||
|
Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
name: true;
|
name: true;
|
||||||
email: true;
|
email: true;
|
||||||
notelp: true;
|
notelp: true;
|
||||||
alasan: true;
|
alasan: true;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const permohonanKeberatanInformasi = proxy({
|
const permohonanKeberatanInformasi = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: {} as PermohonanKeberatanInformasiForm,
|
form: {} as PermohonanKeberatanInformasiForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
async create(){
|
async create() {
|
||||||
const cek = templateForm.safeParse(permohonanKeberatanInformasi.create.form);
|
const cek = templateForm.safeParse(
|
||||||
if(!cek.success) {
|
permohonanKeberatanInformasi.create.form
|
||||||
const err = `[${cek.error.issues
|
);
|
||||||
.map((v) => `${v.path.join(".")}`)
|
if (!cek.success) {
|
||||||
.join("\n")}] required`;
|
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
|
||||||
return toast.error(err);
|
return false; // ⬅️ tambahkan return false
|
||||||
}
|
|
||||||
try {
|
|
||||||
permohonanKeberatanInformasi.create.loading = true;
|
|
||||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
|
|
||||||
if (res.status === 200) {
|
|
||||||
permohonanKeberatanInformasi.findMany.load();
|
|
||||||
return toast.success("Sukses menambahkan");
|
|
||||||
}
|
|
||||||
return toast.error("failed create");
|
|
||||||
} catch (error) {
|
|
||||||
console.log((error as Error).message);
|
|
||||||
} finally {
|
|
||||||
permohonanKeberatanInformasi.create.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
findMany: {
|
|
||||||
data: null as
|
|
||||||
| Prisma.FormulirPermohonanKeberatanGetPayload<{omit: {isActive: true}}>[]
|
|
||||||
| null,
|
|
||||||
async load() {
|
|
||||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["find-many"].get();
|
|
||||||
if (res.status === 200) {
|
|
||||||
permohonanKeberatanInformasi.findMany.data = res.data?.data ?? [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
findUnique: {
|
|
||||||
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
|
|
||||||
omit: {
|
|
||||||
isActive: true;
|
|
||||||
};
|
|
||||||
}> | null,
|
|
||||||
async load(id: string) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
|
|
||||||
} else {
|
|
||||||
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
|
|
||||||
permohonanKeberatanInformasi.findUnique.data = null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching permohonan keberatan informasi:", error);
|
|
||||||
permohonanKeberatanInformasi.findUnique.data = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
permohonanKeberatanInformasi.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
|
||||||
|
"create"
|
||||||
|
].post(permohonanKeberatanInformasi.create.form);
|
||||||
|
if (res.data?.success === false) {
|
||||||
|
toast.error(res.data?.message);
|
||||||
|
return false; // ⬅️ gagal
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Sukses menambahkan");
|
||||||
|
return true; // ⬅️ sukses
|
||||||
|
} catch {
|
||||||
|
toast.error("Terjadi kesalahan server");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
permohonanKeberatanInformasi.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: null as
|
||||||
|
| null
|
||||||
|
| Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||||
|
omit: { isActive: true };
|
||||||
|
}>[],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
total: 0,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
// Change to arrow function
|
||||||
|
permohonanKeberatanInformasi.findMany.loading = true; // Use the full path to access the property
|
||||||
|
permohonanKeberatanInformasi.findMany.page = page;
|
||||||
|
permohonanKeberatanInformasi.findMany.search = search;
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[
|
||||||
|
"find-many"
|
||||||
|
].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
permohonanKeberatanInformasi.findMany.data = res.data.data || [];
|
||||||
|
permohonanKeberatanInformasi.findMany.total = res.data.total || 0;
|
||||||
|
permohonanKeberatanInformasi.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
|
||||||
|
permohonanKeberatanInformasi.findMany.data = [];
|
||||||
|
permohonanKeberatanInformasi.findMany.total = 0;
|
||||||
|
permohonanKeberatanInformasi.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading permohonan keberatan informasi:", error);
|
||||||
|
permohonanKeberatanInformasi.findMany.data = [];
|
||||||
|
permohonanKeberatanInformasi.findMany.total = 0;
|
||||||
|
permohonanKeberatanInformasi.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
permohonanKeberatanInformasi.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.FormulirPermohonanKeberatanGetPayload<{
|
||||||
|
omit: {
|
||||||
|
isActive: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/ppid/permohonankeberataninformasipublik/${id}`
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
permohonanKeberatanInformasi.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to fetch permohonan keberatan informasi:",
|
||||||
|
res.statusText
|
||||||
|
);
|
||||||
|
permohonanKeberatanInformasi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching permohonan keberatan informasi:", error);
|
||||||
|
permohonanKeberatanInformasi.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default permohonanKeberatanInformasi;
|
export default permohonanKeberatanInformasi;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { apiFetchLogin } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||||
import { apiFetchLogin } from '@/app/api/[auth]/_lib/api_fetch_auth';
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Image, Paper, Stack, Title } from '@mantine/core';
|
import { Box, Button, Center, Image, Paper, Stack, Title } from '@mantine/core';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// app/registrasi/page.tsx
|
// app/registrasi/page.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { apiFetchRegister } from '@/app/api/[auth]/_lib/api_fetch_auth';
|
import { apiFetchRegister } from '@/app/api/auth/_lib/api_fetch_auth';
|
||||||
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +18,7 @@ export default function Registrasi() {
|
|||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
|
const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
|
||||||
|
const [agree, setAgree] = useState(false)
|
||||||
|
|
||||||
// Ambil data dari localStorage (dari login)
|
// Ambil data dari localStorage (dari login)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -46,6 +47,11 @@ export default function Registrasi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!agree) {
|
||||||
|
toast.error("Anda harus menyetujui syarat dan ketentuan!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// ✅ Hanya kirim username & nomor → dapat kodeId
|
// ✅ Hanya kirim username & nomor → dapat kodeId
|
||||||
@@ -92,8 +98,8 @@ export default function Registrasi() {
|
|||||||
username.length > 0 && username.length < 5
|
username.length > 0 && username.length < 5
|
||||||
? 'Minimal 5 karakter!'
|
? 'Minimal 5 karakter!'
|
||||||
: username.includes(' ')
|
: username.includes(' ')
|
||||||
? 'Tidak boleh ada spasi'
|
? 'Tidak boleh ada spasi'
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -108,9 +114,29 @@ export default function Registrasi() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box pt="md">
|
<Box pt="md">
|
||||||
<Checkbox label="Saya menyetujui syarat dan ketentuan" defaultChecked />
|
<Checkbox
|
||||||
|
checked={agree}
|
||||||
|
onChange={(e) => setAgree(e.currentTarget.checked)}
|
||||||
|
label={
|
||||||
|
<Text fz="sm">
|
||||||
|
Saya menyetujui{" "}
|
||||||
|
<a
|
||||||
|
href="/terms-of-service"
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
color: colors["blue-button"],
|
||||||
|
textDecoration: "underline",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
syarat dan ketentuan
|
||||||
|
</a>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
<Box pt="xl">
|
<Box pt="xl">
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { authStore } from '@/store/authStore';
|
|||||||
|
|
||||||
export default function Validasi() {
|
export default function Validasi() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [nomor, setNomor] = useState<string | null>(null);
|
const [nomor, setNomor] = useState<string | null>(null);
|
||||||
const [otp, setOtp] = useState('');
|
const [otp, setOtp] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -31,11 +31,11 @@ export default function Validasi() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkFlow = async () => {
|
const checkFlow = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/get-flow', {
|
const res = await fetch('/api/auth/get-flow', {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setIsRegistrationFlow(data.flow === 'register');
|
setIsRegistrationFlow(data.flow === 'register');
|
||||||
console.log('🔍 Flow detected from cookie:', data.flow);
|
console.log('🔍 Flow detected from cookie:', data.flow);
|
||||||
@@ -45,7 +45,7 @@ export default function Validasi() {
|
|||||||
setIsRegistrationFlow(false);
|
setIsRegistrationFlow(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkFlow();
|
checkFlow();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export default function Validasi() {
|
|||||||
setKodeId(storedKodeId);
|
setKodeId(storedKodeId);
|
||||||
const loadOtpData = async () => {
|
const loadOtpData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`);
|
const res = await fetch(`/api/auth/otp-data?kodeId=${encodeURIComponent(storedKodeId)}`);
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
|
|
||||||
if (res.ok && result.data?.nomor) {
|
if (res.ok && result.data?.nomor) {
|
||||||
@@ -110,7 +110,8 @@ export default function Validasi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const verifyRes = await fetch('/api/verify-otp-register', {
|
// ✅ Verify OTP
|
||||||
|
const verifyRes = await fetch('/api/auth/verify-otp-register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
|
body: JSON.stringify({ nomor: cleanNomor, otp, kodeId }),
|
||||||
@@ -123,26 +124,32 @@ export default function Validasi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalizeRes = await fetch('/api/finalize-registration', {
|
// ✅ Finalize registration
|
||||||
|
const finalizeRes = await fetch('/api/auth/finalize-registration', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor, username, kodeId }),
|
body: JSON.stringify({ nomor: cleanNomor, username, kodeId }),
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await finalizeRes.json();
|
const data = await finalizeRes.json();
|
||||||
|
|
||||||
if (data.success || finalizeRes.redirected) {
|
// ✅ Check JSON response (bukan redirect)
|
||||||
// ✅ Cleanup setelah registrasi sukses
|
if (data.success) {
|
||||||
|
toast.success('Registrasi berhasil! Menunggu persetujuan admin.');
|
||||||
await cleanupStorage();
|
await cleanupStorage();
|
||||||
window.location.href = '/waiting-room';
|
|
||||||
|
// ✅ Client-side redirect
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/waiting-room';
|
||||||
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message || 'Registrasi gagal');
|
toast.error(data.message || 'Registrasi gagal');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginVerification = async () => {
|
const handleLoginVerification = async () => {
|
||||||
const loginRes = await fetch('/api/verify-otp-login', {
|
const loginRes = await fetch('/api/auth/verify-otp-login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor, otp, kodeId }),
|
body: JSON.stringify({ nomor, otp, kodeId }),
|
||||||
@@ -197,10 +204,10 @@ export default function Validasi() {
|
|||||||
localStorage.removeItem('auth_kodeId');
|
localStorage.removeItem('auth_kodeId');
|
||||||
localStorage.removeItem('auth_nomor');
|
localStorage.removeItem('auth_nomor');
|
||||||
localStorage.removeItem('auth_username');
|
localStorage.removeItem('auth_username');
|
||||||
|
|
||||||
// Clear cookie
|
// Clear cookie
|
||||||
try {
|
try {
|
||||||
await fetch('/api/clear-flow', {
|
await fetch('/api/auth/clear-flow', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
@@ -212,7 +219,7 @@ export default function Validasi() {
|
|||||||
const handleResend = async () => {
|
const handleResend = async () => {
|
||||||
if (!nomor) return;
|
if (!nomor) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/resend', {
|
const res = await fetch('/api/auth/resend', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ nomor }),
|
body: JSON.stringify({ nomor }),
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ function EditKategoriBerita() {
|
|||||||
{/* Form Wrapper */}
|
{/* Form Wrapper */}
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: '100%', md: '50%' }}
|
w={{ base: '100%', md: '50%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
|
|||||||
303
src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
Normal file
303
src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
|
||||||
|
import stateGallery from "@/app/admin/(dashboard)/_state/desa/gallery";
|
||||||
|
import colors from "@/con/colors";
|
||||||
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
|
import {
|
||||||
|
IconArrowBack,
|
||||||
|
IconPhoto,
|
||||||
|
IconUpload,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useProxy } from "valtio/utils";
|
||||||
|
|
||||||
|
function EditFoto() {
|
||||||
|
const FotoState = useProxy(stateGallery.foto);
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
deskripsi: "",
|
||||||
|
imagesId: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [originalData, setOriginalData] = useState({
|
||||||
|
name: "",
|
||||||
|
deskripsi: "",
|
||||||
|
imagesId: "",
|
||||||
|
imageUrl: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load kategori + Foto
|
||||||
|
useEffect(() => {
|
||||||
|
FotoState.findMany.load();
|
||||||
|
|
||||||
|
const loadFoto = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await FotoState.update.load(id);
|
||||||
|
if (data) {
|
||||||
|
setFormData({
|
||||||
|
name: data.name || "",
|
||||||
|
deskripsi: data.deskripsi || "",
|
||||||
|
imagesId: data.imagesId || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
setOriginalData({
|
||||||
|
name: data.name || "",
|
||||||
|
deskripsi: data.deskripsi || "",
|
||||||
|
imagesId: data.imagesId || "",
|
||||||
|
imageUrl: data.imageGalleryFoto?.link || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data?.imageGalleryFoto?.link) {
|
||||||
|
setPreviewImage(data.imageGalleryFoto.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading Foto:", error);
|
||||||
|
toast.error("Gagal memuat data Foto");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadFoto();
|
||||||
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleChange = (field: string, value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
// Update global state hanya sekali di sini
|
||||||
|
FotoState.update.form = {
|
||||||
|
...FotoState.update.form,
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
|
if (!uploaded?.id) {
|
||||||
|
return toast.error("Gagal upload gambar");
|
||||||
|
}
|
||||||
|
|
||||||
|
FotoState.update.form.imagesId = uploaded.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await FotoState.update.update();
|
||||||
|
toast.success("Foto berhasil diperbarui!");
|
||||||
|
router.push("/admin/desa/gallery/foto");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating foto:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat memperbarui foto");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
name: originalData.name,
|
||||||
|
deskripsi: originalData.deskripsi,
|
||||||
|
imagesId: originalData.imagesId,
|
||||||
|
});
|
||||||
|
setPreviewImage(originalData.imageUrl || null);
|
||||||
|
setFile(null);
|
||||||
|
toast.info("Form dikembalikan ke data awal");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
|
{/* Header */}
|
||||||
|
<Group mb="md">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Foto
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: "100%", md: "50%" }}
|
||||||
|
bg={colors["white-1"]}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: "1px solid #e0e0e0" }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<TextInput
|
||||||
|
label="Judul Foto"
|
||||||
|
placeholder="Masukkan judul foto"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleChange("name", e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Upload Gambar */}
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Foto
|
||||||
|
</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 .png, .jpg, .jpeg, webp
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 220,
|
||||||
|
objectFit: "contain",
|
||||||
|
border: `1px solid ${colors["blue-button"]}`,
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw="bold">
|
||||||
|
Deskripsi Foto
|
||||||
|
</Text>
|
||||||
|
<EditEditor
|
||||||
|
value={formData.deskripsi}
|
||||||
|
onChange={(htmlContent) =>
|
||||||
|
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action */}
|
||||||
|
<Group justify="right">
|
||||||
|
{/* Tombol Batal */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<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)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditFoto;
|
||||||
175
src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx
Normal file
175
src/app/admin/(dashboard)/desa/gallery/foto/[id]/page.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Alert } from '@mantine/core';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack, IconEdit, IconTrash, IconPhoto } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
|
|
||||||
|
function DetailFoto() {
|
||||||
|
const FotoState = useProxy(stateGallery.foto);
|
||||||
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
FotoState.findUnique.load(params?.id as string);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleHapus = () => {
|
||||||
|
if (selectedId) {
|
||||||
|
FotoState.delete.byId(selectedId);
|
||||||
|
setModalHapus(false);
|
||||||
|
setSelectedId(null);
|
||||||
|
router.push("/admin/desa/gallery/foto");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!FotoState.findUnique.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={500} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = FotoState.findUnique.data;
|
||||||
|
const imageUrl = data.imageGalleryFoto?.link;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
|
mb={15}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
// Gunakan max-width agar tidak terlalu lebar di desktop
|
||||||
|
maw={800}
|
||||||
|
w="100%"
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz={{ base: 'xl', md: '2xl' }} fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Foto
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Judul Foto</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
|
<Text
|
||||||
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
|
{imageUrl ? (
|
||||||
|
<Box
|
||||||
|
pos="relative"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '600px', // Set a maximum width
|
||||||
|
margin: '0 auto', // Center the container
|
||||||
|
aspectRatio: '16/9', // Use 16:9 aspect ratio
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={imageUrl}
|
||||||
|
alt={data.name || 'Gambar Foto'}
|
||||||
|
fill
|
||||||
|
style={{
|
||||||
|
objectFit: 'contain', // Changed from 'cover' to 'contain' to show full image
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
onError={() => setImageError(true)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : imageError ? (
|
||||||
|
<Alert
|
||||||
|
color="orange"
|
||||||
|
icon={<IconPhoto size={16} />}
|
||||||
|
title="Gagal memuat gambar"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
Gambar tidak dapat ditampilkan.
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Group gap="sm" justify="flex-start">
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(data.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
onClick={() => router.push(`/admin/desa/gallery/foto/${data.id}/edit`)}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text="Apakah Anda yakin ingin menghapus foto ini?"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailFoto;
|
||||||
228
src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
Normal file
228
src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
'use client';
|
||||||
|
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||||
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Loader,
|
||||||
|
Image
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function CreateFoto() {
|
||||||
|
const FotoState = useProxy(stateGallery.foto);
|
||||||
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
FotoState.create.form = {
|
||||||
|
name: '',
|
||||||
|
deskripsi: '',
|
||||||
|
imagesId: '',
|
||||||
|
};
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
if (!file) {
|
||||||
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
if (!uploaded?.id) {
|
||||||
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
|
}
|
||||||
|
|
||||||
|
FotoState.create.form.imagesId = uploaded.id;
|
||||||
|
|
||||||
|
await FotoState.create.create();
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/desa/gallery/foto');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating foto:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat membuat foto');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
|
{/* Header Back Button + Title */}
|
||||||
|
<Group mb="md">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Foto
|
||||||
|
</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">
|
||||||
|
{/* Judul */}
|
||||||
|
<TextInput
|
||||||
|
label="Judul Foto"
|
||||||
|
placeholder="Masukkan judul Foto"
|
||||||
|
value={FotoState.create.form.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
FotoState.create.form.name = e.currentTarget.value;
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Gambar Berita
|
||||||
|
</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/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
>
|
||||||
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
|
<Dropzone.Accept>
|
||||||
|
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Accept>
|
||||||
|
<Dropzone.Reject>
|
||||||
|
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
|
||||||
|
</Dropzone.Reject>
|
||||||
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
</Group>
|
||||||
|
<Text ta="center" mt="sm" size="sm" color="dimmed">
|
||||||
|
Seret gambar atau klik untuk memilih file (maks 5MB)
|
||||||
|
</Text>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview Gambar"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 200,
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tombol hapus (pojok kanan atas) */}
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
|
Deskripsi Foto
|
||||||
|
</Text>
|
||||||
|
<CreateEditor
|
||||||
|
value={FotoState.create.form.deskripsi}
|
||||||
|
onChange={(val) => {
|
||||||
|
FotoState.create.form.deskripsi = val;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Button Submit */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={resetForm}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<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)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateFoto;
|
||||||
@@ -1,157 +1,163 @@
|
|||||||
"use client";
|
'use client'
|
||||||
import stateFileStorage from "@/state/state-list-image";
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Button,
|
||||||
Flex,
|
Center,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
|
||||||
Pagination,
|
Pagination,
|
||||||
Paper,
|
Paper,
|
||||||
SimpleGrid,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
Title
|
Title
|
||||||
} from "@mantine/core";
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { motion } from "framer-motion";
|
import { useRouter } from 'next/navigation';
|
||||||
import toast from "react-simple-toasts";
|
import { useState } from 'react';
|
||||||
import { useSnapshot } from "valtio";
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import HeaderSearch from '../../../_com/header';
|
||||||
export default function ListImage() {
|
import stateGallery from '../../../_state/desa/gallery';
|
||||||
const { list, total } = useSnapshot(stateFileStorage);
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
stateFileStorage.load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
let timeOut: NodeJS.Timer;
|
|
||||||
|
|
||||||
|
function Foto() {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
return (
|
return (
|
||||||
<Stack p="lg" gap="lg">
|
<Box>
|
||||||
<Flex justify="space-between" align="center" wrap="wrap" gap="md">
|
<HeaderSearch
|
||||||
<Title order={2} fw={700}>
|
title='Foto'
|
||||||
Galeri Foto
|
placeholder='Cari judul atau deskripsi foto...'
|
||||||
</Title>
|
searchIcon={<IconSearch size={20} />}
|
||||||
<TextInput
|
value={search}
|
||||||
radius="xl"
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
size="md"
|
/>
|
||||||
placeholder="Cari foto berdasarkan nama..."
|
<ListFoto search={search} />
|
||||||
leftSection={<IconSearch size={18} />}
|
</Box>
|
||||||
rightSection={
|
|
||||||
<ActionIcon
|
|
||||||
variant="light"
|
|
||||||
color="gray"
|
|
||||||
radius="xl"
|
|
||||||
onClick={() => stateFileStorage.load()}
|
|
||||||
>
|
|
||||||
<IconX size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (timeOut) clearTimeout(timeOut);
|
|
||||||
timeOut = setTimeout(() => {
|
|
||||||
stateFileStorage.load({ search: e.target.value });
|
|
||||||
}, 300);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Paper withBorder radius="lg" p="md" shadow="sm">
|
|
||||||
{list && list.length > 0 ? (
|
|
||||||
<SimpleGrid
|
|
||||||
cols={{ base: 2, sm: 3, md: 5, lg: 8 }}
|
|
||||||
spacing="md"
|
|
||||||
verticalSpacing="md"
|
|
||||||
>
|
|
||||||
{list.map((v, k) => (
|
|
||||||
<Card
|
|
||||||
key={k}
|
|
||||||
withBorder
|
|
||||||
radius="md"
|
|
||||||
shadow="sm"
|
|
||||||
className="hover:shadow-md transition-all duration-200"
|
|
||||||
>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<motion.div
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(v.url);
|
|
||||||
toast("Tautan foto berhasil disalin");
|
|
||||||
}}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={`${v.url}?size=200`}
|
|
||||||
alt={v.name}
|
|
||||||
radius="md"
|
|
||||||
h={120}
|
|
||||||
fit="cover"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Text size="sm" fw={500} lineClamp={2}>
|
|
||||||
{v.name}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Group justify="space-between" align="center" pt="xs">
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
color="red"
|
|
||||||
radius="md"
|
|
||||||
onClick={() => {
|
|
||||||
stateFileStorage
|
|
||||||
.del({ id: v.id })
|
|
||||||
.finally(() => toast("Foto berhasil dihapus"));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconTrash size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
) : (
|
|
||||||
<Stack align="center" justify="center" py="xl" gap="sm">
|
|
||||||
<Image
|
|
||||||
src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png"
|
|
||||||
alt="Kosong"
|
|
||||||
w={120}
|
|
||||||
h={120}
|
|
||||||
fit="contain"
|
|
||||||
opacity={0.7}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<Text c="dimmed" ta="center">
|
|
||||||
Belum ada foto yang tersedia
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{total && total > 1 && (
|
|
||||||
<Flex justify="center">
|
|
||||||
<Pagination
|
|
||||||
total={total}
|
|
||||||
value={stateFileStorage.page} // Changed from page to value
|
|
||||||
size="md"
|
|
||||||
radius="md"
|
|
||||||
withEdges
|
|
||||||
onChange={(page) => {
|
|
||||||
stateFileStorage.load({ page });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ListFoto({ search }: { search: string }) {
|
||||||
|
const FotoState = useProxy(stateGallery.foto)
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = FotoState.findMany;
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
load(page, 10, search)
|
||||||
|
}, [page, search])
|
||||||
|
|
||||||
|
const filteredData = data || []
|
||||||
|
|
||||||
|
if (loading || !data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={600} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box py={10}>
|
||||||
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={4}>Daftar Foto</Title>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/desa/gallery/foto/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh style={{ width: '25%' }}>Judul Foto</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
|
||||||
|
<TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
|
||||||
|
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd style={{ width: '25%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '20%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{new Date(item.createdAt).toLocaleDateString('id-ID', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '30%' }}>
|
||||||
|
<Box w={200}>
|
||||||
|
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '15%' }}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => router.push(`/admin/desa/gallery/foto/${item.id}`)}
|
||||||
|
>
|
||||||
|
<IconDeviceImac size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text c="dimmed">Tidak ada foto yang cocok</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10)
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
mt="md"
|
||||||
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Foto;
|
||||||
|
|||||||
@@ -11,21 +11,21 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
|||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "Profile Desa",
|
label: "Profil Desa",
|
||||||
value: "profiledesa",
|
value: "profildesa",
|
||||||
href: "/admin/desa/profile/profile-desa",
|
href: "/admin/desa/profil/profil-desa",
|
||||||
icon: <IconUser size={18} stroke={1.8} />
|
icon: <IconUser size={18} stroke={1.8} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Profile Perbekel",
|
label: "Profil Perbekel",
|
||||||
value: "profileperbekel",
|
value: "profilperbekel",
|
||||||
href: "/admin/desa/profile/profile-perbekel",
|
href: "/admin/desa/profil/profil-perbekel",
|
||||||
icon: <IconUsers size={18} stroke={1.8} />
|
icon: <IconUsers size={18} stroke={1.8} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Profile Perbekel Dari Masa Ke Masa",
|
label: "Profil Perbekel Dari Masa Ke Masa",
|
||||||
value: "profile-perbekel-dari-masa-ke-masa",
|
value: "profilperbekeldarimasakemasa",
|
||||||
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
|
href: "/admin/desa/profil/profil-perbekel-dari-masa-ke-masa",
|
||||||
icon: <IconCalendar size={18} stroke={1.8} />
|
icon: <IconCalendar size={18} stroke={1.8} />
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -12,22 +12,22 @@ function LayoutTabsEdit({ children }: { children: React.ReactNode }) {
|
|||||||
{
|
{
|
||||||
label: "Sejarah Desa",
|
label: "Sejarah Desa",
|
||||||
value: "sejarahdesa",
|
value: "sejarahdesa",
|
||||||
href: "/admin/desa/profile/edit/sejarah_desa"
|
href: "/admin/desa/profil/edit/sejarah_desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Visi Misi Desa",
|
label: "Visi Misi Desa",
|
||||||
value: "visimisidesa",
|
value: "visimisidesa",
|
||||||
href: "/admin/desa/profile/edit/visi_misi_desa"
|
href: "/admin/desa/profil/edit/visi_misi_desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Lambang Desa",
|
label: "Lambang Desa",
|
||||||
value: "lambangdesa",
|
value: "lambangdesa",
|
||||||
href: "/admin/desa/profile/edit/lambang_desa"
|
href: "/admin/desa/profil/edit/lambang_desa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Maskot Desa",
|
label: "Maskot Desa",
|
||||||
value: "maskotdesa",
|
value: "maskotdesa",
|
||||||
href: "/admin/desa/profile/edit/maskot_desa"
|
href: "/admin/desa/profil/edit/maskot_desa"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||||
@@ -43,7 +43,7 @@ function Page() {
|
|||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error('ID tidak valid');
|
toast.error('ID tidak valid');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ function Page() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success('Data berhasil disimpan');
|
toast.success('Data berhasil disimpan');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
} else {
|
} else {
|
||||||
toast.error('Gagal menyimpan data');
|
toast.error('Gagal menyimpan data');
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ function Page() {
|
|||||||
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
|
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
|
||||||
{loadError}
|
{loadError}
|
||||||
</Alert>
|
</Alert>
|
||||||
<Button onClick={() => router.push('/admin/desa/profile/profile-desa')} variant="outline">
|
<Button onClick={() => router.push('/admin/desa/profil/profil-desa')} variant="outline">
|
||||||
Kembali ke Halaman Utama
|
Kembali ke Halaman Utama
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -40,7 +40,7 @@ function Page() {
|
|||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error("ID tidak valid");
|
toast.error("ID tidak valid");
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
router.push("/admin/desa/profil/profil-desa");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ function Page() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Maskot berhasil diperbarui!");
|
toast.success("Maskot berhasil diperbarui!");
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
router.push("/admin/desa/profil/profil-desa");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error update maskot:", error);
|
console.error("Error update maskot:", error);
|
||||||
@@ -50,7 +50,7 @@ function Page() {
|
|||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error('ID tidak valid');
|
toast.error('ID tidak valid');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ function Page() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success('Data berhasil disimpan');
|
toast.success('Data berhasil disimpan');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
} else {
|
} else {
|
||||||
toast.error('Gagal menyimpan data');
|
toast.error('Gagal menyimpan data');
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ function Page() {
|
|||||||
{loadError}
|
{loadError}
|
||||||
</Alert>
|
</Alert>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push('/admin/desa/profile/profile-desa')}
|
onClick={() => router.push('/admin/desa/profil/profil-desa')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Kembali ke Halaman Utama
|
Kembali ke Halaman Utama
|
||||||
@@ -42,7 +42,7 @@ function Page() {
|
|||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error('ID tidak valid');
|
toast.error('ID tidak valid');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ function Page() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success('Data berhasil disimpan');
|
toast.success('Data berhasil disimpan');
|
||||||
router.push('/admin/desa/profile/profile-desa');
|
router.push('/admin/desa/profil/profil-desa');
|
||||||
} else {
|
} else {
|
||||||
toast.error('Gagal menyimpan data');
|
toast.error('Gagal menyimpan data');
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ function Page() {
|
|||||||
{loadError}
|
{loadError}
|
||||||
</Alert>
|
</Alert>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push('/admin/desa/profile/profile-desa')}
|
onClick={() => router.push('/admin/desa/profil/profil-desa')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Kembali ke Halaman Utama
|
Kembali ke Halaman Utama
|
||||||
@@ -27,7 +27,7 @@ function Page() {
|
|||||||
return (
|
return (
|
||||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={2} c={colors['blue-button']}>Preview Profile Desa</Title>
|
<Title order={2} c={colors['blue-button']}>Preview Profil Desa</Title>
|
||||||
|
|
||||||
{/* Sejarah Desa */}
|
{/* Sejarah Desa */}
|
||||||
{sejarah && (
|
{sejarah && (
|
||||||
@@ -42,7 +42,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-desa/${sejarah.id}/sejarah_desa`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -87,7 +87,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-desa/${visiMisi.id}/visi_misi_desa`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -135,7 +135,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-desa/${lambang.id}/lambang_desa`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -180,7 +180,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-desa/${maskot.id}/maskot_desa`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -117,7 +117,7 @@ function EditPerbekelDariMasaKeMasa() {
|
|||||||
|
|
||||||
await state.update.update();
|
await state.update.update();
|
||||||
toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
|
toast.success('Perbekel dari masa ke masa berhasil diperbarui!');
|
||||||
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating perbekel dari masa ke masa:', error);
|
console.error('Error updating perbekel dari masa ke masa:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
|
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
|
||||||
@@ -4,7 +4,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
|||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Button, Group, 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';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -25,7 +25,7 @@ function DetailPerbekelDariMasa() {
|
|||||||
state.delete.byId(selectedId);
|
state.delete.byId(selectedId);
|
||||||
setModalHapus(false);
|
setModalHapus(false);
|
||||||
setSelectedId(null);
|
setSelectedId(null);
|
||||||
router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa");
|
router.push("/admin/desa/profil/profil-perbekel-dari-masa-ke-masa");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,12 +108,12 @@ function DetailPerbekelDariMasa() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -46,7 +46,7 @@ function CreatePerbekelDariMasaKeMasa() {
|
|||||||
state.create.form.imageId = uploaded.id;
|
state.create.form.imageId = uploaded.id;
|
||||||
await state.create.create();
|
await state.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error('Gagal menambahkan perbekel dari masa ke masa');
|
toast.error('Gagal menambahkan perbekel dari masa ke masa');
|
||||||
@@ -53,7 +53,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
|||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/create')}
|
onClick={() => router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/create')}
|
||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
@@ -90,7 +90,7 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${item.id}`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-perbekel-dari-masa-ke-masa/${item.id}`)}
|
||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
@@ -25,7 +25,7 @@ function ProfilePerbekel() {
|
|||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error("ID tidak valid");
|
toast.error("ID tidak valid");
|
||||||
router.push("/admin/desa/profile/profile-perbekel");
|
router.push("/admin/desa/profil/profil-perbekel");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ function ProfilePerbekel() {
|
|||||||
const success = await perbekelState.edit.submit()
|
const success = await perbekelState.edit.submit()
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Data berhasil disimpan");
|
toast.success("Data berhasil disimpan");
|
||||||
router.push("/admin/desa/profile/profile-perbekel");
|
router.push("/admin/desa/profil/profil-perbekel");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error update sejarah desa:", error);
|
console.error("Error update sejarah desa:", error);
|
||||||
@@ -41,7 +41,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${perbekel.id}`)}
|
onClick={() => router.push(`/admin/desa/profil/profil-perbekel/${perbekel.id}`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
@@ -44,18 +44,56 @@ function CreatePolsekTerdekat() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isValidGoogleMapsEmbed = (url: string): boolean => {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
return (
|
||||||
|
u.hostname === 'www.google.com' &&
|
||||||
|
u.pathname === '/maps/embed' &&
|
||||||
|
u.searchParams.has('pb')
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
const { embedMapUrl } = polsekState.create.form;
|
||||||
setIsSubmitting(true);
|
|
||||||
await polsekState.create.create();
|
// ✅ Validasi Google Maps Embed URL (jika diisi)
|
||||||
resetForm();
|
if (embedMapUrl && !isValidGoogleMapsEmbed(embedMapUrl)) {
|
||||||
router.push("/admin/keamanan/polsek-terdekat");
|
toast.error("URL embed peta tidak valid. Harap paste iframe dari Google Maps.");
|
||||||
} catch (error) {
|
return;
|
||||||
console.error(error)
|
}
|
||||||
toast.error("Gagal menambah polsek terdekat");
|
|
||||||
} finally {
|
try {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(true);
|
||||||
|
await polsekState.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push("/admin/keamanan/polsek-terdekat");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Gagal menambah polsek terdekat");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractEmbedUrl = (input: string): string => {
|
||||||
|
// Jika sudah berupa URL embed yang valid
|
||||||
|
if (input.startsWith('https://www.google.com/maps/embed?')) {
|
||||||
|
return input.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Coba parse sebagai HTML string (iframe)
|
||||||
|
const iframeRegex = /<iframe[^>]*src=["']([^"']*)["'][^>]*>/i;
|
||||||
|
const match = input.match(iframeRegex);
|
||||||
|
if (match && match[1]?.startsWith('https://www.google.com/maps/embed?')) {
|
||||||
|
return match[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak cocok, kembalikan input asli (atau string kosong)
|
||||||
|
return input.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchLayanan = async () => {
|
const fetchLayanan = async () => {
|
||||||
@@ -190,9 +228,14 @@ function CreatePolsekTerdekat() {
|
|||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={polsekState.create.form.embedMapUrl}
|
value={polsekState.create.form.embedMapUrl}
|
||||||
onChange={(val) => (polsekState.create.form.embedMapUrl = val.target.value)}
|
onChange={(e) => {
|
||||||
|
const rawValue = e.currentTarget.value;
|
||||||
|
const cleanUrl = extractEmbedUrl(rawValue);
|
||||||
|
polsekState.create.form.embedMapUrl = cleanUrl;
|
||||||
|
}}
|
||||||
|
description="Contoh: https://www.google.com/maps/embed?pb=..."
|
||||||
label={<Text fw="bold" fz="sm">Embed Map URL</Text>}
|
label={<Text fw="bold" fz="sm">Embed Map URL</Text>}
|
||||||
placeholder="Masukkan embed map url"
|
placeholder="Paste iframe dari Google Maps atau URL embed langsung"
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={polsekState.create.form.namaTempatMaps}
|
value={polsekState.create.form.namaTempatMaps}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
icon: <IconActivity size={18} stroke={1.8} />
|
icon: <IconActivity size={18} stroke={1.8} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Grafik Hasil Kepuasan Masyarakat",
|
label: "Penderita Penyakit",
|
||||||
value: "grafikhasilkepuasan",
|
value: "penderitapenyakit",
|
||||||
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
|
href: "/admin/kesehatan/data-kesehatan-warga/penderita_penyakit",
|
||||||
icon: <IconGauge size={18} stroke={1.8} />
|
icon: <IconGauge size={18} stroke={1.8} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
@@ -10,19 +8,22 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
|
MultiSelect,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
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';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
interface FasilitasKesehatanFormBase {
|
// Tipe form yang SESUAI dengan logika relasi (array ID)
|
||||||
|
interface EditFasilitasKesehatanForm {
|
||||||
name: string;
|
name: string;
|
||||||
informasiUmum: {
|
informasiUmum: {
|
||||||
fasilitas: string;
|
fasilitas: string;
|
||||||
@@ -30,128 +31,92 @@ interface FasilitasKesehatanFormBase {
|
|||||||
jamOperasional: string;
|
jamOperasional: string;
|
||||||
};
|
};
|
||||||
layananUnggulan: { content: string };
|
layananUnggulan: { content: string };
|
||||||
dokterdanTenagaMedis: {
|
dokterdanTenagaMedis: string[]; // ← ARRAY ID
|
||||||
name: string;
|
|
||||||
specialist: string;
|
|
||||||
jadwal: string;
|
|
||||||
};
|
|
||||||
fasilitasPendukung: { content: string };
|
fasilitasPendukung: { content: string };
|
||||||
prosedurPendaftaran: { content: string };
|
prosedurPendaftaran: { content: string };
|
||||||
tarifDanLayanan: {
|
tarifDanLayanan: string[]; // ← ARRAY ID
|
||||||
layanan: string;
|
|
||||||
tarif: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditFasilitasKesehatan() {
|
function EditFasilitasKesehatan() {
|
||||||
const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
|
const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
|
||||||
|
const dokterState = useProxy(fasilitasKesehatanState.dokter);
|
||||||
|
const tarifState = useProxy(fasilitasKesehatanState.tarif);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams<{ id: string }>();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const [formData, setFormData] = useState<FasilitasKesehatanFormBase>({
|
const [formData, setFormData] = useState<EditFasilitasKesehatanForm>({
|
||||||
name: '',
|
name: '',
|
||||||
informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' },
|
informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' },
|
||||||
layananUnggulan: { content: '' },
|
layananUnggulan: { content: '' },
|
||||||
dokterdanTenagaMedis: { name: '', specialist: '', jadwal: '' },
|
dokterdanTenagaMedis: [],
|
||||||
fasilitasPendukung: { content: '' },
|
fasilitasPendukung: { content: '' },
|
||||||
prosedurPendaftaran: { content: '' },
|
prosedurPendaftaran: { content: '' },
|
||||||
tarifDanLayanan: { layanan: '', tarif: '' },
|
tarifDanLayanan: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState<FasilitasKesehatanFormBase>({
|
// Load data fasilitas & daftar dokter/tarif
|
||||||
name: '',
|
useShallowEffect(() => {
|
||||||
informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' },
|
const loadAll = async () => {
|
||||||
layananUnggulan: { content: '' },
|
const id = params?.id;
|
||||||
dokterdanTenagaMedis: { name: '', specialist: '', jadwal: '' },
|
if (!id) return;
|
||||||
fasilitasPendukung: { content: '' },
|
|
||||||
prosedurPendaftaran: { content: '' },
|
|
||||||
tarifDanLayanan: { layanan: '', tarif: '' },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper untuk update nested state
|
// Load dokter & tarif (untuk opsi MultiSelect)
|
||||||
const updateForm = <K extends keyof FasilitasKesehatanFormBase>(
|
await Promise.all([
|
||||||
key: K,
|
dokterState.findMany.load(),
|
||||||
value: FasilitasKesehatanFormBase[K]
|
tarifState.findMany.load(),
|
||||||
) => setFormData(prev => ({ ...prev, [key]: value }));
|
]);
|
||||||
|
|
||||||
const updateNested = <
|
// Load data fasilitas
|
||||||
K extends keyof FasilitasKesehatanFormBase,
|
await state.edit.load(id);
|
||||||
N extends keyof FasilitasKesehatanFormBase[K]
|
const loaded = state.edit.form;
|
||||||
>(key: K, nestedKey: N, value: FasilitasKesehatanFormBase[K][N]) =>
|
if (loaded) {
|
||||||
setFormData(prev => ({
|
setFormData({
|
||||||
...prev,
|
name: loaded.name,
|
||||||
[key]: { ...prev[key] as object, [nestedKey]: value },
|
informasiUmum: loaded.informasiUmum,
|
||||||
}));
|
layananUnggulan: loaded.layananUnggulan,
|
||||||
|
dokterdanTenagaMedis: loaded.dokterdanTenagaMedis || [],
|
||||||
|
fasilitasPendukung: loaded.fasilitasPendukung,
|
||||||
|
prosedurPendaftaran: loaded.prosedurPendaftaran,
|
||||||
|
tarifDanLayanan: loaded.tarifDanLayanan || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deepClone = (obj: any): any => {
|
loadAll();
|
||||||
try {
|
}, [params?.id]);
|
||||||
return JSON.parse(JSON.stringify(obj));
|
|
||||||
} catch (error) {
|
const updateForm = <K extends keyof EditFasilitasKesehatanForm>(
|
||||||
console.warn('Gagal deep clone dengan JSON fallback:', error);
|
field: K,
|
||||||
return obj; // fallback (berisiko shared reference)
|
value: EditFasilitasKesehatanForm[K]
|
||||||
|
) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
const loaded = state.edit.form;
|
||||||
|
if (loaded) {
|
||||||
|
setFormData({
|
||||||
|
name: loaded.name,
|
||||||
|
informasiUmum: loaded.informasiUmum,
|
||||||
|
layananUnggulan: loaded.layananUnggulan,
|
||||||
|
dokterdanTenagaMedis: loaded.dokterdanTenagaMedis || [],
|
||||||
|
fasilitasPendukung: loaded.fasilitasPendukung,
|
||||||
|
prosedurPendaftaran: loaded.prosedurPendaftaran,
|
||||||
|
tarifDanLayanan: loaded.tarifDanLayanan || [],
|
||||||
|
});
|
||||||
|
toast.info('Form dikembalikan ke data awal');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load data
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
useEffect(() => {
|
e.preventDefault();
|
||||||
const load = async () => {
|
|
||||||
const id = params?.id as string;
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await state.edit.load(id);
|
|
||||||
const loadedData = state.edit.form;
|
|
||||||
|
|
||||||
if (!loadedData) {
|
|
||||||
toast.error('Data tidak ditemukan');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gunakan JSON fallback untuk deep clone
|
|
||||||
const clonedData = deepClone(loadedData) as FasilitasKesehatanFormBase;
|
|
||||||
|
|
||||||
setFormData(clonedData);
|
|
||||||
setOriginalData(clonedData);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
toast.error('Gagal memuat data fasilitas kesehatan');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
load();
|
|
||||||
}, [params?.id]);
|
|
||||||
|
|
||||||
const handleResetForm = () => {
|
|
||||||
setFormData({
|
|
||||||
name: originalData.name,
|
|
||||||
informasiUmum:
|
|
||||||
{
|
|
||||||
fasilitas: originalData.informasiUmum.fasilitas,
|
|
||||||
alamat: originalData.informasiUmum.alamat,
|
|
||||||
jamOperasional: originalData.informasiUmum.jamOperasional
|
|
||||||
},
|
|
||||||
layananUnggulan: { content: originalData.layananUnggulan.content },
|
|
||||||
dokterdanTenagaMedis: {
|
|
||||||
name: originalData.dokterdanTenagaMedis.name,
|
|
||||||
specialist: originalData.dokterdanTenagaMedis.specialist,
|
|
||||||
jadwal: originalData.dokterdanTenagaMedis.jadwal
|
|
||||||
},
|
|
||||||
fasilitasPendukung: { content: originalData.fasilitasPendukung.content },
|
|
||||||
prosedurPendaftaran: { content: originalData.prosedurPendaftaran.content },
|
|
||||||
tarifDanLayanan: {
|
|
||||||
layanan: originalData.tarifDanLayanan.layanan,
|
|
||||||
tarif: originalData.tarifDanLayanan.tarif
|
|
||||||
},
|
|
||||||
});
|
|
||||||
toast.info("Form dikembalikan ke data awal");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Submit
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
state.edit.form = { ...state.edit.form, ...formData };
|
|
||||||
|
// Update state Valtio
|
||||||
|
state.edit.form = { ...formData };
|
||||||
|
|
||||||
const success = await state.edit.submit();
|
const success = await state.edit.submit();
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success('Fasilitas kesehatan berhasil diperbarui!');
|
toast.success('Fasilitas kesehatan berhasil diperbarui!');
|
||||||
@@ -159,14 +124,14 @@ function EditFasilitasKesehatan() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui data fasilitas kesehatan');
|
toast.error('Gagal memperbarui data');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
@@ -189,7 +154,7 @@ function EditFasilitasKesehatan() {
|
|||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Nama Fasilitas Kesehatan"
|
label="Nama Fasilitas Kesehatan"
|
||||||
placeholder="Masukkan nama fasilitas kesehatan"
|
placeholder="Masukkan nama"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => updateForm('name', e.target.value)}
|
onChange={(e) => updateForm('name', e.target.value)}
|
||||||
required
|
required
|
||||||
@@ -197,118 +162,108 @@ function EditFasilitasKesehatan() {
|
|||||||
|
|
||||||
{/* Informasi Umum */}
|
{/* Informasi Umum */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" mb={5}>
|
<Text fw="bold" mb={5}>Informasi Umum</Text>
|
||||||
Informasi Umum
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Fasilitas"
|
label="Fasilitas"
|
||||||
value={formData.informasiUmum.fasilitas}
|
value={formData.informasiUmum.fasilitas}
|
||||||
onChange={(e) => updateNested('informasiUmum', 'fasilitas', e.target.value)}
|
onChange={(e) =>
|
||||||
|
updateForm('informasiUmum', {
|
||||||
|
...formData.informasiUmum,
|
||||||
|
fasilitas: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Alamat"
|
label="Alamat"
|
||||||
value={formData.informasiUmum.alamat}
|
value={formData.informasiUmum.alamat}
|
||||||
onChange={(e) => updateNested('informasiUmum', 'alamat', e.target.value)}
|
onChange={(e) =>
|
||||||
|
updateForm('informasiUmum', {
|
||||||
|
...formData.informasiUmum,
|
||||||
|
alamat: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Jam Operasional"
|
label="Jam Operasional"
|
||||||
value={formData.informasiUmum.jamOperasional}
|
value={formData.informasiUmum.jamOperasional}
|
||||||
onChange={(e) => updateNested('informasiUmum', 'jamOperasional', e.target.value)}
|
onChange={(e) =>
|
||||||
|
updateForm('informasiUmum', {
|
||||||
|
...formData.informasiUmum,
|
||||||
|
jamOperasional: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Layanan Unggulan */}
|
{/* Layanan Unggulan */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" mb={5}>
|
<Text fw="bold" mb={5}>Layanan Unggulan</Text>
|
||||||
Layanan Unggulan
|
|
||||||
</Text>
|
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.layananUnggulan.content}
|
value={formData.layananUnggulan.content}
|
||||||
onChange={(v) => updateNested('layananUnggulan', 'content', v)}
|
onChange={(v) => updateForm('layananUnggulan', { content: v })}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Dokter dan Tenaga Medis */}
|
{/* Dokter & Tenaga Medis — MultiSelect */}
|
||||||
<Box>
|
<MultiSelect
|
||||||
<Text fw="bold" mb={5}>
|
label="Dokter & Tenaga Medis"
|
||||||
Dokter dan Tenaga Medis
|
placeholder="Pilih dokter/tenaga medis"
|
||||||
</Text>
|
data={
|
||||||
<TextInput
|
dokterState.findMany.data?.map((d) => ({
|
||||||
label="Nama Dokter"
|
value: d.id,
|
||||||
value={formData.dokterdanTenagaMedis.name}
|
label: `${d.name} (${d.specialist})`,
|
||||||
onChange={(e) => updateNested('dokterdanTenagaMedis', 'name', e.target.value)}
|
})) || []
|
||||||
/>
|
}
|
||||||
<TextInput
|
value={formData.dokterdanTenagaMedis}
|
||||||
label="Specialist"
|
onChange={(val) => updateForm('dokterdanTenagaMedis', val)}
|
||||||
value={formData.dokterdanTenagaMedis.specialist}
|
searchable
|
||||||
onChange={(e) =>
|
clearable
|
||||||
updateNested('dokterdanTenagaMedis', 'specialist', e.target.value)
|
required
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Jadwal"
|
|
||||||
value={formData.dokterdanTenagaMedis.jadwal}
|
|
||||||
onChange={(e) => updateNested('dokterdanTenagaMedis', 'jadwal', e.target.value)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Fasilitas Pendukung */}
|
{/* Fasilitas Pendukung */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" mb={5}>
|
<Text fw="bold" mb={5}>Fasilitas Pendukung</Text>
|
||||||
Fasilitas Pendukung
|
|
||||||
</Text>
|
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.fasilitasPendukung.content}
|
value={formData.fasilitasPendukung.content}
|
||||||
onChange={(v) => updateNested('fasilitasPendukung', 'content', v)}
|
onChange={(v) => updateForm('fasilitasPendukung', { content: v })}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Prosedur Pendaftaran */}
|
{/* Prosedur Pendaftaran */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" mb={5}>
|
<Text fw="bold" mb={5}>Prosedur Pendaftaran</Text>
|
||||||
Prosedur Pendaftaran
|
|
||||||
</Text>
|
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.prosedurPendaftaran.content}
|
value={formData.prosedurPendaftaran.content}
|
||||||
onChange={(v) => updateNested('prosedurPendaftaran', 'content', v)}
|
onChange={(v) => updateForm('prosedurPendaftaran', { content: v })}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Tarif dan Layanan */}
|
{/* Tarif & Layanan — MultiSelect */}
|
||||||
<Box>
|
<MultiSelect
|
||||||
<Text fw="bold" mb={5}>
|
label="Tarif & Layanan"
|
||||||
Tarif dan Layanan
|
placeholder="Pilih layanan"
|
||||||
</Text>
|
data={
|
||||||
<TextInput
|
tarifState.findMany.data?.map((t) => ({
|
||||||
label="Tarif"
|
value: t.id,
|
||||||
value={formData.tarifDanLayanan.tarif}
|
label: `${t.layanan} - ${t.tarif}`,
|
||||||
onChange={(e) => updateNested('tarifDanLayanan', 'tarif', e.target.value)}
|
})) || []
|
||||||
/>
|
}
|
||||||
<TextInput
|
value={formData.tarifDanLayanan}
|
||||||
label="Layanan"
|
onChange={(val) => updateForm('tarifDanLayanan', val)}
|
||||||
value={formData.tarifDanLayanan.layanan}
|
searchable
|
||||||
onChange={(e) => updateNested('tarifDanLayanan', 'layanan', e.target.value)}
|
clearable
|
||||||
/>
|
required
|
||||||
</Box>
|
/>
|
||||||
|
|
||||||
{/* Tombol Simpan */}
|
{/* Aksi */}
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
{/* Tombol Batal */}
|
<Button variant="outline" color="gray" radius="md" onClick={handleReset}>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
color="gray"
|
|
||||||
radius="md"
|
|
||||||
size="md"
|
|
||||||
onClick={handleResetForm}
|
|
||||||
>
|
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Tombol Simpan */}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
type="submit"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
@@ -324,4 +279,4 @@ function EditFasilitasKesehatan() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditFasilitasKesehatan;
|
export default EditFasilitasKesehatan;
|
||||||
@@ -9,6 +9,12 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
Text
|
Text
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
@@ -108,21 +114,79 @@ function DetailFasilitasKesehatan() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold">Dokter & Tenaga Medis</Text>
|
<Text fz="lg" fw="bold" mb="sm">Dokter & Tenaga Medis</Text>
|
||||||
<Text fz="md" fw="bold">Nama</Text>
|
{Array.isArray(data.dokterdantenagamedis) && data.dokterdantenagamedis.length > 0 ? (
|
||||||
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.name || '-'}</Text>
|
<Box style={{ overflowX: 'auto', width: '100%' }}>
|
||||||
<Text fz="md" fw="bold">Spesialis</Text>
|
<Table striped highlightOnHover withTableBorder>
|
||||||
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.specialist || '-'}</Text>
|
<TableThead>
|
||||||
<Text fz="md" fw="bold">Jadwal</Text>
|
<TableTr>
|
||||||
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.jadwal || '-'}</Text>
|
<TableTh style={{ whiteSpace: 'nowrap' }}>Nama</TableTh>
|
||||||
|
<TableTh style={{ whiteSpace: 'nowrap' }}>Spesialis</TableTh>
|
||||||
|
<TableTh style={{ whiteSpace: 'nowrap' }}>Jadwal</TableTh>
|
||||||
|
<TableTh style={{ whiteSpace: 'nowrap' }}>Jam Operasional</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{data.dokterdantenagamedis.map((dokter) => (
|
||||||
|
<TableTr key={dokter.id}>
|
||||||
|
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
|
||||||
|
{dokter.name || '-'}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
|
||||||
|
{dokter.specialist || '-'}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
|
||||||
|
{dokter.jadwal || '-'}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
|
||||||
|
{dokter.jamBukaOperasional} – {dokter.jamTutupOperasional}
|
||||||
|
{dokter.jadwalLibur && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<Text span c="dimmed" fz="xs">
|
||||||
|
Libur: {dokter.jadwalLibur} ({dokter.jamBukaLibur}–{dokter.jamTutupLibur})
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed">Tidak ada dokter atau tenaga medis terdaftar.</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box mt="xl">
|
||||||
<Text fz="lg" fw="bold">Tarif & Layanan</Text>
|
<Text fz="lg" fw="bold" mb="sm">Tarif & Layanan</Text>
|
||||||
<Text fz="md" fw="bold">Layanan</Text>
|
{Array.isArray(data.tarifdanlayanan) && data.tarifdanlayanan.length > 0 ? (
|
||||||
<Text fz="md" c="dimmed">{data.tarifdanlayanan?.layanan || '-'}</Text>
|
<Box style={{ overflowX: 'auto', width: '100%' }}>
|
||||||
<Text fz="md" fw="bold">Tarif</Text>
|
<Table striped highlightOnHover withTableBorder>
|
||||||
<Text fz="md" c="dimmed">{data.tarifdanlayanan?.tarif || '-'}</Text>
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh style={{ whiteSpace: 'nowrap' }}>Layanan</TableTh>
|
||||||
|
<TableTh style={{ whiteSpace: 'nowrap' }}>Tarif</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{data.tarifdanlayanan.map((tarif) => (
|
||||||
|
<TableTr key={tarif.id}>
|
||||||
|
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
|
||||||
|
{tarif.layanan || '-'}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
|
||||||
|
{tarif.tarif || '-'}
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed">Tidak ada tarif atau layanan terdaftar.</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Aksi */}
|
{/* Aksi */}
|
||||||
|
|||||||
@@ -7,19 +7,20 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
|
MultiSelect,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title
|
Title
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
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 { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
function CreateFasilitasKesehatan() {
|
function CreateFasilitasKesehatan() {
|
||||||
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
|
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -34,23 +35,16 @@ function CreateFasilitasKesehatan() {
|
|||||||
jamOperasional: '',
|
jamOperasional: '',
|
||||||
},
|
},
|
||||||
layananUnggulan: {
|
layananUnggulan: {
|
||||||
content: '',
|
content: ''
|
||||||
},
|
|
||||||
dokterdanTenagaMedis: {
|
|
||||||
name: '',
|
|
||||||
specialist: '',
|
|
||||||
jadwal: '',
|
|
||||||
},
|
},
|
||||||
|
dokterdanTenagaMedis: [] as string[],
|
||||||
fasilitasPendukung: {
|
fasilitasPendukung: {
|
||||||
content: '',
|
content: '',
|
||||||
},
|
},
|
||||||
prosedurPendaftaran: {
|
prosedurPendaftaran: {
|
||||||
content: '',
|
content: '',
|
||||||
},
|
},
|
||||||
tarifDanLayanan: {
|
tarifDanLayanan: [] as string[],
|
||||||
layanan: '',
|
|
||||||
tarif: '',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,6 +64,11 @@ function CreateFasilitasKesehatan() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
fasilitasKesehatanState.dokter.findMany.load();
|
||||||
|
fasilitasKesehatanState.tarif.findMany.load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -140,31 +139,25 @@ function CreateFasilitasKesehatan() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
{/* Dokter dan Tenaga Medis */}
|
{/* Dokter dan Tenaga Medis */}
|
||||||
<Box>
|
<MultiSelect
|
||||||
<Text fz="md" fw="bold" mb={5}>Dokter dan Tenaga Medis</Text>
|
label="Dokter & Tenaga Medis"
|
||||||
<TextInput
|
placeholder="Pilih dokter / tenaga medis"
|
||||||
label="Nama Dokter"
|
data={
|
||||||
placeholder="Masukkan nama dokter"
|
fasilitasKesehatanState.dokter.findMany.data?.map((item) => ({
|
||||||
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name}
|
label: item.name,
|
||||||
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name = e.target.value)}
|
value: item.id,
|
||||||
required
|
})) || []
|
||||||
/>
|
}
|
||||||
<TextInput
|
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis}
|
||||||
label="Spesialis"
|
onChange={(val: string[]) => {
|
||||||
placeholder="Masukkan spesialis"
|
stateFasilitasKesehatan.create.form.dokterdanTenagaMedis = val;
|
||||||
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist}
|
}}
|
||||||
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist = e.target.value)}
|
searchable
|
||||||
required
|
clearable
|
||||||
/>
|
required
|
||||||
<TextInput
|
/>
|
||||||
label="Jadwal"
|
|
||||||
placeholder="Masukkan jadwal"
|
|
||||||
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal}
|
|
||||||
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal = e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Fasilitas Pendukung */}
|
{/* Fasilitas Pendukung */}
|
||||||
<Box>
|
<Box>
|
||||||
@@ -175,6 +168,24 @@ function CreateFasilitasKesehatan() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<MultiSelect
|
||||||
|
label="Layanan"
|
||||||
|
placeholder="Pilih layanan"
|
||||||
|
data={
|
||||||
|
fasilitasKesehatanState.tarif.findMany.data?.map((item) => ({
|
||||||
|
label: item.layanan,
|
||||||
|
value: item.id,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
value={stateFasilitasKesehatan.create.form.tarifDanLayanan} // string[]
|
||||||
|
onChange={(val: string[]) => {
|
||||||
|
stateFasilitasKesehatan.create.form.tarifDanLayanan = val;
|
||||||
|
}}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Prosedur Pendaftaran */}
|
{/* Prosedur Pendaftaran */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="md" fw="bold" mb={5}>Prosedur Pendaftaran</Text>
|
<Text fz="md" fw="bold" mb={5}>Prosedur Pendaftaran</Text>
|
||||||
@@ -184,24 +195,6 @@ function CreateFasilitasKesehatan() {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Tarif dan Layanan */}
|
|
||||||
<Box>
|
|
||||||
<Text fz="md" fw="bold" mb={5}>Tarif dan Layanan</Text>
|
|
||||||
<TextInput
|
|
||||||
label="Tarif"
|
|
||||||
placeholder="Masukkan tarif"
|
|
||||||
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif}
|
|
||||||
onChange={(e) => (stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif = e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Layanan"
|
|
||||||
placeholder="Masukkan layanan"
|
|
||||||
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan}
|
|
||||||
onChange={(e) => (stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan = e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
|||||||
@@ -1,11 +1,241 @@
|
|||||||
import React from 'react';
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { TimeInput } from '@mantine/dates';
|
||||||
|
import { IconArrowBack, IconClock } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
|
function EditDokterTenagaMedis() {
|
||||||
|
const state = useProxy(fasilitasKesehatanState.dokter);
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
specialist: '',
|
||||||
|
jadwal: '',
|
||||||
|
jadwalLibur: '',
|
||||||
|
jamBukaOperasional: '',
|
||||||
|
jamTutupOperasional: '',
|
||||||
|
jamBukaLibur: '',
|
||||||
|
jamTutupLibur: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [originalData, setOriginalData] = useState({
|
||||||
|
name: '',
|
||||||
|
specialist: '',
|
||||||
|
jadwal: '',
|
||||||
|
jadwalLibur: '',
|
||||||
|
jamBukaOperasional: '',
|
||||||
|
jamTutupOperasional: '',
|
||||||
|
jamBukaLibur: '',
|
||||||
|
jamTutupLibur: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load data
|
||||||
|
useEffect(() => {
|
||||||
|
const load = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await state.update.load(id);
|
||||||
|
const loadedData = state.update.form;
|
||||||
|
|
||||||
|
if (!loadedData) {
|
||||||
|
toast.error('Data tidak ditemukan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormData(loadedData);
|
||||||
|
setOriginalData(loadedData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast.error('Gagal memuat data fasilitas kesehatan');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
name: originalData.name,
|
||||||
|
specialist: originalData.specialist,
|
||||||
|
jadwal: originalData.jadwal,
|
||||||
|
jadwalLibur: originalData.jadwalLibur,
|
||||||
|
jamBukaOperasional: originalData.jamBukaOperasional,
|
||||||
|
jamTutupOperasional: originalData.jamTutupOperasional,
|
||||||
|
jamBukaLibur: originalData.jamBukaLibur,
|
||||||
|
jamTutupLibur: originalData.jamTutupLibur,
|
||||||
|
});
|
||||||
|
toast.info("Form dikembalikan ke data awal");
|
||||||
|
};
|
||||||
|
|
||||||
|
const refBuka = useRef<HTMLInputElement>(null);
|
||||||
|
const refTutup = useRef<HTMLInputElement>(null);
|
||||||
|
const refBukaLibur = useRef<HTMLInputElement>(null);
|
||||||
|
const refTutupLibur = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const picker = (ref: any) => (
|
||||||
|
<ActionIcon variant="subtle" color="gray" onClick={() => ref.current?.showPicker()}>
|
||||||
|
<IconClock size={16} stroke={1.5} />
|
||||||
|
</ActionIcon>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (field: string, value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
state.update.form = { ...state.update.form, ...formData };
|
||||||
|
const success = await state.update.submit();
|
||||||
|
if (success) {
|
||||||
|
toast.success('Data berhasil diperbarui!');
|
||||||
|
router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast.error('Terjadi kesalahan saat memperbarui data');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function Page() {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
Page
|
{/* Header */}
|
||||||
</div>
|
<Group mb="md">
|
||||||
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Fasilitas Kesehatan
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<TextInput
|
||||||
|
label="Nama Dokter"
|
||||||
|
placeholder="Masukkan nama dokter"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleChange("name", e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Jadwal"
|
||||||
|
placeholder="Masukkan jadwal"
|
||||||
|
value={formData.jadwal}
|
||||||
|
onChange={(e) => handleChange("jadwal", e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Jadwal Libur"
|
||||||
|
placeholder="Masukkan jadwal libur"
|
||||||
|
value={formData.jadwalLibur}
|
||||||
|
onChange={(e) => handleChange("jadwalLibur", e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimeInput
|
||||||
|
label="Jam Buka Operasional"
|
||||||
|
ref={refBuka}
|
||||||
|
rightSection={picker(refBuka)}
|
||||||
|
value={formData.jamBukaOperasional}
|
||||||
|
onChange={(e) => handleChange("jamBukaOperasional", e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimeInput
|
||||||
|
label="Jam Tutup Operasional"
|
||||||
|
ref={refTutup}
|
||||||
|
rightSection={picker(refTutup)}
|
||||||
|
value={formData.jamTutupOperasional}
|
||||||
|
onChange={(e) => handleChange("jamTutupOperasional", e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimeInput
|
||||||
|
label="Jam Buka Hari Libur"
|
||||||
|
ref={refBukaLibur}
|
||||||
|
rightSection={picker(refBukaLibur)}
|
||||||
|
value={formData.jamBukaLibur}
|
||||||
|
onChange={(e) => handleChange("jamBukaLibur", e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimeInput
|
||||||
|
label="Jam Tutup Hari Libur"
|
||||||
|
ref={refTutupLibur}
|
||||||
|
rightSection={picker(refTutupLibur)}
|
||||||
|
value={formData.jamTutupLibur}
|
||||||
|
onChange={(e) => handleChange("jamTutupLibur", e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<Group justify="right">
|
||||||
|
{/* Tombol Batal */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<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)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
export default EditDokterTenagaMedis;
|
||||||
|
|||||||
@@ -1,11 +1,165 @@
|
|||||||
import React from 'react';
|
'use client'
|
||||||
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function DetailDokterTenagaMedis() {
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const state = useProxy(fasilitasKesehatanState.dokter);
|
||||||
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
state.findUnique.load(params?.id as string);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleHapus = () => {
|
||||||
|
if (selectedId) {
|
||||||
|
state.delete.byId(selectedId);
|
||||||
|
setModalHapus(false);
|
||||||
|
setSelectedId(null);
|
||||||
|
router.push(
|
||||||
|
'/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!state.findUnique.data) {
|
||||||
|
return (
|
||||||
|
<Stack py={10}>
|
||||||
|
<Skeleton height={500} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = state.findUnique.data;
|
||||||
|
|
||||||
function Page() {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Box py={10}>
|
||||||
Page
|
{/* Tombol Back */}
|
||||||
</div>
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
|
||||||
|
mb={15}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Wrapper Detail */}
|
||||||
|
<Paper
|
||||||
|
withBorder
|
||||||
|
w={{ base: '100%', md: '70%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
|
Detail Dokter & Tenaga Medis
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Nama Dokter</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="md" fw="bold">Specialist</Text>
|
||||||
|
<Text fz="md" c="dimmed">{data.specialist || '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Jadwal</Text>
|
||||||
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jadwal || '-' }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Jadwal Libur</Text>
|
||||||
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jadwalLibur || '-' }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Jam Buka Operasional</Text>
|
||||||
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jamBukaOperasional || '-' }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Jam Tutup Operasional</Text>
|
||||||
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jamTutupOperasional || '-' }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Jam Buka Libur</Text>
|
||||||
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jamBukaLibur || '-' }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="lg" fw="bold">Jam Tutup Libur</Text>
|
||||||
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.jamTutupLibur || '-' }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Aksi */}
|
||||||
|
<Group gap="sm">
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(data.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconTrash size={20} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/${data.id}/edit`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="light"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconEdit size={20} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Modal Konfirmasi Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text="Apakah anda yakin ingin menghapus dokter & tenaga medis ini?"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
export default DetailDokterTenagaMedis;
|
||||||
|
|||||||
@@ -1,71 +1,184 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'use client'
|
'use client'
|
||||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
|
||||||
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { ActionIcon, Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { TimeInput } from '@mantine/dates';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { IconArrowBack, IconClock } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
|
||||||
function CreateDokter() {
|
function CreateDokter() {
|
||||||
const params = useParams()
|
|
||||||
const createState = useProxy(fasilitasKesehatanState.dokter)
|
const createState = useProxy(fasilitasKesehatanState.dokter)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
createState.create.create.form = {
|
createState.create.create.form = {
|
||||||
name: "",
|
name: "",
|
||||||
specialist: "",
|
specialist: "",
|
||||||
jadwal: "",
|
jadwal: "",
|
||||||
|
jadwalLibur: "",
|
||||||
|
jamBukaOperasional: "",
|
||||||
|
jamTutupOperasional: "",
|
||||||
|
jamBukaLibur: "",
|
||||||
|
jamTutupLibur: "",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refBuka = useRef<HTMLInputElement>(null);
|
||||||
|
const refTutup = useRef<HTMLInputElement>(null);
|
||||||
|
const refBukaLibur = useRef<HTMLInputElement>(null);
|
||||||
|
const refTutupLibur = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const picker = (ref: any) => (
|
||||||
|
<ActionIcon variant="subtle" color="gray" onClick={() => ref.current?.showPicker()}>
|
||||||
|
<IconClock size={16} stroke={1.5} />
|
||||||
|
</ActionIcon>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create.create();
|
try {
|
||||||
resetForm();
|
setIsSubmitting(true);
|
||||||
router.push(`/admin/kesehatan/fasilitas-kesehatan/${params?.id}/dokter-tenaga-medis`)
|
await createState.create.create.create();
|
||||||
|
toast.success('Data berhasil disimpan');
|
||||||
|
resetForm();
|
||||||
|
router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error('Gagal menyimpan data');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Box component="form" onSubmit={handleSubmit}>
|
<Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
|
||||||
<Box mb={10}>
|
{/* Header */}
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Group mb="md">
|
||||||
<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>
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Data Dokter & Tenaga Medis
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
|
{/* Form */}
|
||||||
<Stack gap="xs">
|
<Paper
|
||||||
<Title order={3}>Create Dokter</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 fz="sm" fw="bold">Nama Dokter</Text>}
|
label={"Nama Dokter"}
|
||||||
placeholder="masukkan nama dokter"
|
placeholder="Masukkan nama dokter"
|
||||||
value={createState.create.create.form.name}
|
value={createState.create.create.form.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => (createState.create.create.form.name = e.target.value)}
|
||||||
createState.create.create.form.name = e.target.value;
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Text fz="md" fw="bold">Specialist</Text>
|
|
||||||
|
{/* Informasi Umum */}
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fz="sm" fw="bold">Specialist</Text>}
|
label="Specialist"
|
||||||
placeholder="masukkan specialist"
|
placeholder="Masukkan specialist"
|
||||||
value={createState.create.create.form.specialist}
|
value={createState.create.create.form.specialist}
|
||||||
onChange={(e) => {
|
onChange={(e) => (createState.create.create.form.specialist = e.target.value)}
|
||||||
createState.create.create.form.specialist = e.target.value;
|
required
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Box>
|
|
||||||
<Text fz="md" fw="bold">Jadwal</Text>
|
<TextInput
|
||||||
<CreateEditor
|
label="Jadwal"
|
||||||
value={createState.create.create.form.jadwal}
|
placeholder="Masukkan jadwal"
|
||||||
onChange={(htmlContent) => {
|
value={createState.create.create.form.jadwal}
|
||||||
createState.create.create.form.jadwal = htmlContent;
|
onChange={(e) => (createState.create.create.form.jadwal = e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Jadwal Libur"
|
||||||
|
placeholder="Masukkan jadwal libur"
|
||||||
|
value={createState.create.create.form.jadwalLibur}
|
||||||
|
onChange={(e) => (createState.create.create.form.jadwalLibur = e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimeInput
|
||||||
|
label="Jam Buka Operasional"
|
||||||
|
ref={refBuka}
|
||||||
|
rightSection={picker(refBuka)}
|
||||||
|
value={createState.create.create.form.jamBukaOperasional}
|
||||||
|
onChange={(e) => (createState.create.create.form.jamBukaOperasional = e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimeInput
|
||||||
|
label="Jam Tutup Operasional"
|
||||||
|
ref={refTutup}
|
||||||
|
rightSection={picker(refTutup)}
|
||||||
|
value={createState.create.create.form.jamTutupOperasional}
|
||||||
|
onChange={(e) => (createState.create.create.form.jamTutupOperasional = e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimeInput
|
||||||
|
label="Jam Buka Hari Libur"
|
||||||
|
ref={refBukaLibur}
|
||||||
|
rightSection={picker(refBukaLibur)}
|
||||||
|
value={createState.create.create.form.jamBukaLibur}
|
||||||
|
onChange={(e) => (createState.create.create.form.jamBukaLibur = e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimeInput
|
||||||
|
label="Jam Tutup Hari Libur"
|
||||||
|
ref={refTutupLibur}
|
||||||
|
rightSection={picker(refTutupLibur)}
|
||||||
|
value={createState.create.create.form.jamTutupLibur}
|
||||||
|
onChange={(e) => (createState.create.create.form.jamTutupLibur = e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
|
<Group justify="right">
|
||||||
|
{/* Tombol Batal */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={resetForm}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<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)',
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Box>
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
<Button onClick={handleSubmit} bg={colors['blue-button']}>
|
</Button>
|
||||||
Simpan
|
</Group>
|
||||||
</Button>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
'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 { Box, Button, Center, Group, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } 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 HeaderSearch from '@/app/admin/(dashboard)/_com/header';
|
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
|
||||||
import JudulList from '@/app/admin/(dashboard)/_com/judulList';
|
|
||||||
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ function DokterTenagaMedis() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box mb={10}>
|
<Box mb={10}>
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -60,49 +59,101 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
|
|||||||
}
|
}
|
||||||
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">
|
||||||
<Stack>
|
{/* Judul + Tombol Tambah */}
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Fasilitas Kesehatan'
|
<Title order={4}>Daftar Dokter dan Tenaga Medis</Title>
|
||||||
href={`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create`}
|
<Button
|
||||||
/>
|
leftSection={<IconPlus size={18} />}
|
||||||
<Box style={{ overflowX: "auto" }}>
|
color="blue"
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
variant="light"
|
||||||
<TableThead>
|
onClick={() =>
|
||||||
<TableTr>
|
router.push(
|
||||||
<TableTh>Fasilitas Kesehatan</TableTh>
|
'/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create'
|
||||||
<TableTh>Alamat</TableTh>
|
)
|
||||||
<TableTh>Jam Operasional</TableTh>
|
}
|
||||||
<TableTh>Detail</TableTh>
|
>
|
||||||
</TableTr>
|
Tambah Baru
|
||||||
</TableThead>
|
</Button>
|
||||||
<TableTbody>
|
</Group>
|
||||||
{filteredData.map((item) => (
|
|
||||||
|
{/* Tabel */}
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Nama Dokter</TableTh>
|
||||||
|
<TableTh>Spesialis</TableTh>
|
||||||
|
<TableTh>Jadwal</TableTh>
|
||||||
|
<TableTh>Aksi</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>{item.name}</TableTd>
|
|
||||||
<TableTd>{item.specialist}</TableTd>
|
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Text dangerouslySetInnerHTML={{ __html: item.jadwal }} />
|
<Box w={150}>
|
||||||
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}>
|
<Box w={150}>
|
||||||
<IconDeviceImacCog size={25} />
|
{item.specialist || '-'}
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Box w={150}>
|
||||||
|
<Text dangerouslySetInnerHTML={{ __html: item.jadwal || '-' }} />
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconDeviceImacCog size={20} />
|
||||||
|
<Text ml={5}>Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
</TableTbody>
|
) : (
|
||||||
</Table>
|
<TableTr>
|
||||||
</Box>
|
<TableTd colSpan={4}>
|
||||||
</Stack>
|
<Center py={20}>
|
||||||
|
<Text c="dimmed">
|
||||||
|
Tidak ada fasilitas kesehatan yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)} // ini penting!
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10, search);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
Group,
|
Group,
|
||||||
Pagination,
|
Pagination,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -16,30 +19,52 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Tooltip
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconCoin, IconDeviceImacCog, IconPlus, IconReportMedical, 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 fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||||
|
|
||||||
|
|
||||||
function FasilitasKesehatan() {
|
function FasilitasKesehatan() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const router = useRouter()
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* Header Search */}
|
<Grid mb={10}>
|
||||||
<HeaderSearch
|
<GridCol span={{ base: 12, md: 8 }}>
|
||||||
title='Fasilitas Kesehatan'
|
<Title order={3}>Fasilitas Kesehatan</Title>
|
||||||
placeholder='Cari nama, alamat, atau jam operasional...'
|
</GridCol>
|
||||||
searchIcon={<IconSearch size={20} />}
|
<GridCol span={{ base: 12, md: 4 }}>
|
||||||
value={search}
|
<Group gap={"xs"}>
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
<Tooltip label="List Dokter" withArrow>
|
||||||
/>
|
<ActionIcon onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis')} size="lg" radius="xl" color="green.6">
|
||||||
|
<IconReportMedical size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="List Tarif Layanan" withArrow>
|
||||||
|
<ActionIcon onClick={()=> router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan')} size="lg" radius="xl" color="blue.6">
|
||||||
|
<IconCoin size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Paper radius="lg" bg={colors['white-1']}>
|
||||||
|
<TextInput
|
||||||
|
radius="lg"
|
||||||
|
placeholder='Cari nama, alamat, atau jam operasional...'
|
||||||
|
leftSection={<IconSearch size={20} />}
|
||||||
|
w="133%"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Group>
|
||||||
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<ListFasilitasKesehatan search={search} />
|
<ListFasilitasKesehatan search={search} />
|
||||||
</Box>
|
</Box>
|
||||||
@@ -54,6 +79,7 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
|
|||||||
const { data, page, totalPages, loading, load } = stateFasilitasKesehatan.findMany;
|
const { data, page, totalPages, loading, load } = stateFasilitasKesehatan.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
|
|
||||||
load(page, 10, search);
|
load(page, 10, search);
|
||||||
}, [page, search]);
|
}, [page, search]);
|
||||||
|
|
||||||
@@ -93,8 +119,8 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
|
|||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Fasilitas Kesehatan</TableTh>
|
<TableTh>Fasilitas Kesehatan</TableTh>
|
||||||
<TableTh>Dokter</TableTh>
|
<TableTh>Jumlah Dokter</TableTh>
|
||||||
<TableTh>Layanan</TableTh>
|
<TableTh>Jumlah Layanan</TableTh>
|
||||||
<TableTh>Aksi</TableTh>
|
<TableTh>Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
@@ -111,13 +137,17 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
|
|||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={150}>
|
<Box w={150}>
|
||||||
{item.dokterdantenagamedis?.name || '-'}
|
{item.dokterdantenagamedis?.length
|
||||||
|
? `${item.dokterdantenagamedis.length} dokter`
|
||||||
|
: '-'}
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={150}>
|
<Box w={150}>
|
||||||
<Text truncate="end" lineClamp={1}>
|
<Text truncate="end" lineClamp={1}>
|
||||||
{item.tarifdanlayanan?.layanan || '-'}
|
{item.tarifdanlayanan?.length
|
||||||
|
? `${item.tarifdanlayanan.length} layanan`
|
||||||
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -141,7 +171,7 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
|
|||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={4}>
|
<TableTd colSpan={4}>
|
||||||
<Center py={20}>
|
<Center py={20}>
|
||||||
<Text color="dimmed">
|
<Text c="dimmed">
|
||||||
Tidak ada fasilitas kesehatan yang cocok
|
Tidak ada fasilitas kesehatan yang cocok
|
||||||
</Text>
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function EditTarifLayanan() {
|
||||||
|
const editState = useProxy(fasilitasKesehatanState.tarif);
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [originalData, setOriginalData] = useState({
|
||||||
|
tarif: '',
|
||||||
|
layanan: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
tarif: '',
|
||||||
|
layanan: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTarifLayanan = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await editState.update.load(id);
|
||||||
|
if (data) {
|
||||||
|
setFormData({
|
||||||
|
tarif: data.tarif || '',
|
||||||
|
layanan: data.layanan || '',
|
||||||
|
});
|
||||||
|
setOriginalData({
|
||||||
|
tarif: data.tarif || '',
|
||||||
|
layanan: data.layanan || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading tarif layanan:', error);
|
||||||
|
toast.error('Gagal memuat data tarif layanan');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadTarifLayanan();
|
||||||
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleChange = (field: string, value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
tarif: originalData.tarif,
|
||||||
|
layanan: originalData.layanan,
|
||||||
|
});
|
||||||
|
toast.info('Form dikembalikan ke data awal');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
// update global state hanya saat submit
|
||||||
|
editState.update.form = {
|
||||||
|
...editState.update.form,
|
||||||
|
tarif: formData.tarif,
|
||||||
|
layanan: formData.layanan,
|
||||||
|
};
|
||||||
|
|
||||||
|
await editState.update.submit();
|
||||||
|
toast.success('Tarif Layanan berhasil diperbarui!');
|
||||||
|
router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating tarif layanan:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat memperbarui tarif layanan');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
|
{/* Back Button + Title */}
|
||||||
|
<Group mb="md">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Tarif Layanan
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form Wrapper */}
|
||||||
|
<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
|
||||||
|
name="layanan"
|
||||||
|
label="Layanan"
|
||||||
|
placeholder="Masukkan nama layanan"
|
||||||
|
value={formData.layanan}
|
||||||
|
onChange={(e) => handleChange('layanan', e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
name="tarif"
|
||||||
|
label="Tarif"
|
||||||
|
placeholder="Masukkan tarif layanan"
|
||||||
|
value={formData.tarif}
|
||||||
|
onChange={(e) => handleChange('tarif', e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<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)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditTarifLayanan;
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
'use client';
|
||||||
|
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function CreateTarifLayanan() {
|
||||||
|
const createState = useProxy(fasilitasKesehatanState.tarif);
|
||||||
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
createState.create.form = {
|
||||||
|
tarif: '',
|
||||||
|
layanan: '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await createState.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating tarif layanan:', error);
|
||||||
|
toast.error('Gagal menambahkan tarif layanan');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
|
{/* Header dengan back button */}
|
||||||
|
<Group mb="md">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
p="xs"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Tambah Tarif Layanan
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form utama */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '50%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="lg"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<TextInput
|
||||||
|
label="Layanan"
|
||||||
|
placeholder="Masukkan nama layanan"
|
||||||
|
value={createState.create.form.layanan || ''}
|
||||||
|
onChange={(e) => (createState.create.form.layanan = e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Tarif"
|
||||||
|
placeholder="Masukkan tarif"
|
||||||
|
value={createState.create.form.tarif || ''}
|
||||||
|
onChange={(e) => (createState.create.form.tarif = e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={resetForm}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
|
<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)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateTarifLayanan;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
'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 { Box, Button, Center, Group, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconPlus, IconSearch, IconTrash } 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 HeaderSearch from '@/app/admin/(dashboard)/_com/header';
|
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
|
||||||
import JudulList from '@/app/admin/(dashboard)/_com/judulList';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -18,12 +18,12 @@ function TarifLayanan() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box mb={10}>
|
<Box mb={10}>
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button variant="subtle" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan')}>
|
||||||
<IconArrowBack color={colors['blue-button']} size={25} />
|
<IconArrowBack color={colors['blue-button']} size={25} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Dokter dan Tenaga Medis'
|
title='Tarif dan Layanan'
|
||||||
placeholder='pencarian'
|
placeholder='pencarian'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
@@ -35,8 +35,11 @@ function TarifLayanan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListTarifLayanan({ search }: { search: string }) {
|
function ListTarifLayanan({ search }: { search: string }) {
|
||||||
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.dokter)
|
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.tarif);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [modalHapus, setModalHapus] = useState(false);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
loading,
|
loading,
|
||||||
@@ -49,6 +52,15 @@ function ListTarifLayanan({ search }: { search: string }) {
|
|||||||
load(page, 10, search)
|
load(page, 10, search)
|
||||||
}, [page, search])
|
}, [page, search])
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (selectedId) {
|
||||||
|
stateFasilitasKesehatan.delete.byId(selectedId);
|
||||||
|
setModalHapus(false);
|
||||||
|
setSelectedId(null);
|
||||||
|
load(page, 10, search);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || []
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
@@ -60,51 +72,116 @@ function ListTarifLayanan({ search }: { search: string }) {
|
|||||||
}
|
}
|
||||||
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">
|
||||||
<Stack>
|
{/* Judul + Tombol Tambah */}
|
||||||
<JudulList
|
<Group justify="space-between" mb="md">
|
||||||
title='List Fasilitas Kesehatan'
|
<Title order={4}>Daftar Tarif dan Layanan</Title>
|
||||||
href={`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create`}
|
<Button
|
||||||
/>
|
leftSection={<IconPlus size={18} />}
|
||||||
<Box style={{ overflowX: "auto" }}>
|
color="blue"
|
||||||
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
|
variant="light"
|
||||||
<TableThead>
|
onClick={() =>
|
||||||
<TableTr>
|
router.push(
|
||||||
<TableTh>Fasilitas Kesehatan</TableTh>
|
'/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/create'
|
||||||
<TableTh>Alamat</TableTh>
|
)
|
||||||
<TableTh>Jam Operasional</TableTh>
|
}
|
||||||
<TableTh>Detail</TableTh>
|
>
|
||||||
</TableTr>
|
Tambah Baru
|
||||||
</TableThead>
|
</Button>
|
||||||
<TableTbody>
|
</Group>
|
||||||
{filteredData.map((item) => (
|
|
||||||
|
{/* Tabel */}
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table highlightOnHover>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>Layanan</TableTh>
|
||||||
|
<TableTh>Tarif</TableTh>
|
||||||
|
<TableTh>Edit</TableTh>
|
||||||
|
<TableTh>Hapus</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>{item.name}</TableTd>
|
|
||||||
<TableTd>{item.specialist}</TableTd>
|
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Text dangerouslySetInnerHTML={{ __html: item.jadwal }} />
|
<Box w={150}>
|
||||||
|
{item.layanan || '-'}
|
||||||
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}>
|
<Box w={150}>
|
||||||
<IconDeviceImacCog size={25} />
|
<Text fw={500} truncate="end" lineClamp={1}>
|
||||||
|
{item.tarif}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="green"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconEdit size={18} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
disabled={stateFasilitasKesehatan.delete.loading}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
</TableTbody>
|
) : (
|
||||||
</Table>
|
<TableTr>
|
||||||
</Box>
|
<TableTd colSpan={4}>
|
||||||
</Stack>
|
<Center py={20}>
|
||||||
|
<Text c="dimmed">
|
||||||
|
Tidak ada fasilitas kesehatan yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => load(newPage)} // ini penting!
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10, search);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
mt="md"
|
||||||
mb="md"
|
mb="md"
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
text="Apakah anda yakin ingin menghapus tarif layanan ini?"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ function EditGrafikHasilKepuasan() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error loading grafik hasil kepuasan:", err);
|
console.error("Error loading penderita penyakit:", err);
|
||||||
toast.error("Gagal memuat data grafik hasil kepuasan");
|
toast.error("Gagal memuat data penderita penyakit");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,11 +99,11 @@ function EditGrafikHasilKepuasan() {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
editState.update.form = { ...editState.update.form, ...formData };
|
editState.update.form = { ...editState.update.form, ...formData };
|
||||||
await editState.update.submit();
|
await editState.update.submit();
|
||||||
toast.success('Grafik hasil kepuasan berhasil diperbarui!');
|
toast.success('penderita penyakit berhasil diperbarui!');
|
||||||
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan');
|
router.push('/admin/kesehatan/data-kesehatan-warga/penderita_penyakit');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error updating grafik hasil kepuasan:', err);
|
console.error('Error updating penderita penyakit:', err);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan');
|
toast.error('Terjadi kesalahan saat memperbarui penderita penyakit');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ function EditGrafikHasilKepuasan() {
|
|||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Grafik Hasil Kepuasan
|
Edit Penderita Penyakit
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ function DetailGrafikHasilKepuasan() {
|
|||||||
state.delete.byId(selectedId);
|
state.delete.byId(selectedId);
|
||||||
setModalHapus(false);
|
setModalHapus(false);
|
||||||
setSelectedId(null);
|
setSelectedId(null);
|
||||||
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
|
router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ function DetailGrafikHasilKepuasan() {
|
|||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||||
Detail Data Grafik Hasil Kepuasan
|
Detail Data Penderita Penyakit
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
|
||||||
@@ -118,7 +118,7 @@ function DetailGrafikHasilKepuasan() {
|
|||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${data.id}/edit`
|
`/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${data.id}/edit`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -40,7 +40,7 @@ function CreateGrafikHasilKepuasanMasyarakat() {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
await stateGrafikKepuasan.create.create();
|
await stateGrafikKepuasan.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
|
router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating grafik kepuasan:", error);
|
console.error("Error creating grafik kepuasan:", error);
|
||||||
toast.error("Terjadi kesalahan saat membuat grafik kepuasan");
|
toast.error("Terjadi kesalahan saat membuat grafik kepuasan");
|
||||||
@@ -62,7 +62,7 @@ function CreateGrafikHasilKepuasanMasyarakat() {
|
|||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Grafik Hasil Kepuasan Masyarakat
|
Tambah Penderita Penyakit
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ function GrafikHasilKepuasanMasyarakat() {
|
|||||||
<Box>
|
<Box>
|
||||||
{/* Header Search */}
|
{/* Header Search */}
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title='Grafik Hasil Kepuasan Masyarakat'
|
title='Penderita Penyakit'
|
||||||
placeholder='Cari nama atau alamat...'
|
placeholder='Cari nama atau alamat...'
|
||||||
searchIcon={<IconSearch size={20} />}
|
searchIcon={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
@@ -115,14 +115,14 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
|||||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
{/* Judul + Tombol Tambah */}
|
{/* Judul + Tombol Tambah */}
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={4}>Daftar Grafik Hasil Kepuasan Masyarakat</Title>
|
<Title order={4}>Daftar Penderita Penyakit</Title>
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
'/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create'
|
'/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/create'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -176,7 +176,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
|||||||
color="blue"
|
color="blue"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`
|
`/admin/kesehatan/data-kesehatan-warga/penderita_penyakit/${item.id}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -221,7 +221,7 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
|
|||||||
{/* Chart */}
|
{/* Chart */}
|
||||||
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
|
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p={'md'}>
|
<Paper withBorder bg={colors['white-1']} p={'md'}>
|
||||||
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
|
<Title pb={10} order={4}>Penderita Penyakit</Title>
|
||||||
{mounted && diseaseChartData.length > 0 ? (
|
{mounted && diseaseChartData.length > 0 ? (
|
||||||
<Center>
|
<Center>
|
||||||
<BarChart
|
<BarChart
|
||||||
@@ -123,7 +123,7 @@ export default function EditKolaborasiInovasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={24} />
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Button, Group, 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';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -42,7 +42,7 @@ function DetailSDGSDesa() {
|
|||||||
const data = sdgsState.findUnique.data;
|
const data = sdgsState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -54,7 +54,7 @@ function DetailSDGSDesa() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: '100%', md: '60%' }}
|
w={{ base: '100%', md: '70%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -106,7 +106,7 @@ function DetailSDGSDesa() {
|
|||||||
size="md"
|
size="md"
|
||||||
disabled={sdgsState.delete.loading}
|
disabled={sdgsState.delete.loading}
|
||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function CreateSDGsDesa() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconPlus, 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';
|
||||||
@@ -9,7 +9,6 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import HeaderSearch from '../../_com/header';
|
import HeaderSearch from '../../_com/header';
|
||||||
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
|
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
|
||||||
|
|
||||||
|
|
||||||
function SdgsDesa() {
|
function SdgsDesa() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
@@ -27,8 +26,10 @@ function SdgsDesa() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListSdgsDesa({ search }: { search: string }) {
|
function ListSdgsDesa({ search }: { search: string }) {
|
||||||
const listState = useProxy(sdgsDesa)
|
const listState = useProxy(sdgsDesa);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -39,10 +40,10 @@ function ListSdgsDesa({ search }: { search: string }) {
|
|||||||
} = listState.findMany;
|
} = listState.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, debouncedSearch);
|
||||||
}, [page, search])
|
}, [page, debouncedSearch]);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || [];
|
||||||
|
|
||||||
// Handle loading state
|
// Handle loading state
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
@@ -53,79 +54,71 @@ function ListSdgsDesa({ search }: { search: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.length === 0) {
|
const isEmpty = data.length === 0;
|
||||||
return (
|
|
||||||
<Box py={10}>
|
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
|
||||||
<Group justify="space-between" mb="md">
|
|
||||||
<Title order={4}>Daftar Sdgs Desa</Title>
|
|
||||||
<Button
|
|
||||||
leftSection={<IconPlus size={18} />}
|
|
||||||
color={colors['blue-button']}
|
|
||||||
variant="light"
|
|
||||||
onClick={() => router.push('/admin/landing-page/SDGs/create')}
|
|
||||||
>
|
|
||||||
Tambah Baru
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
<Table highlightOnHover striped verticalSpacing="sm">
|
|
||||||
<TableThead>
|
|
||||||
<TableTr>
|
|
||||||
<TableTh style={{ width: '60%' }}>Nama Sdgs Desa</TableTh>
|
|
||||||
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
|
|
||||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
<TableTr>
|
|
||||||
<TableTd colSpan={3} style={{ textAlign: 'center', padding: '2rem' }}>
|
|
||||||
<Text c="dimmed">Tidak ada data Sdgs Desa</Text>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={{ base: 'sm', md: 'lg' }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||||
<Title order={4}>Daftar Sdgs Desa</Title>
|
<Title order={2} lh={1.2}>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light"
|
Daftar Sdgs Desa
|
||||||
onClick={() => router.push('/admin/landing-page/SDGs/create')}
|
</Title>
|
||||||
>
|
<Button
|
||||||
Tambah Baru
|
leftSection={<IconPlus size={18} />}
|
||||||
</Button>
|
color='blue'
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/landing-page/SDGs/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
<Table highlightOnHover>
|
{/* Desktop Table */}
|
||||||
|
<Box visibleFrom="md">
|
||||||
|
<Table highlightOnHover striped verticalSpacing="sm">
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '60%' }}>Nama Sdgs Desa</TableTh>
|
<TableTh style={{ width: '60%' }}>
|
||||||
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
|
<Text fz="sm" fw={600} c="dark.7" ta="left">
|
||||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
Nama Sdgs Desa
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>
|
||||||
|
<Text fz="sm" fw={600} c="dark.7" ta="left">
|
||||||
|
Jumlah
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh style={{ width: '20%' }}>
|
||||||
|
<Text fz="sm" fw={600} c="dark.7" ta="center">
|
||||||
|
Aksi
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.map((item) => (
|
{isEmpty ? (
|
||||||
<TableTr key={item.id}>
|
<TableTr>
|
||||||
<TableTd style={{ width: '60%' }}>
|
<TableTd colSpan={3} ta="center" py="xl">
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
{item.name}
|
Tidak ada data Sdgs Desa
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%' }}>
|
</TableTr>
|
||||||
<Text fz="sm" c="dimmed">
|
) : (
|
||||||
{item.jumlah || '0'}
|
filteredData.map((item) => (
|
||||||
</Text>
|
<TableTr key={item.id}>
|
||||||
</TableTd>
|
<TableTd style={{ width: '60%' }}>
|
||||||
<TableTd style={{ width: '20%', textAlign: 'center' }}>
|
<Text fz="md" fw={500} truncate="end" lineClamp={1} lh={1.5}>
|
||||||
<Button
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '20%' }}>
|
||||||
|
<Text fz="sm" c="dark.6" lh={1.5}>
|
||||||
|
{item.jumlah || '0'}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '20%' }} ta="center">
|
||||||
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -135,27 +128,75 @@ function ListSdgsDesa({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
{isEmpty ? (
|
||||||
|
<Center py="xl">
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.5} ta="center">
|
||||||
|
Tidak ada data Sdgs Desa
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Stack gap="sm">
|
||||||
|
{filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} withBorder p="md" radius="md">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Nama SDGs Desa</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Jumlah</Text>
|
||||||
|
<Text fz="xs" c="dark.6" lh={1.4}>
|
||||||
|
{item.jumlah || '0'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Group justify="flex-end" mt="xs">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() => router.push(`/admin/landing-page/SDGs/${item.id}`)}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center mt="lg">
|
|
||||||
<Pagination
|
{!isEmpty && (
|
||||||
value={page}
|
<Center mt={{ base: 'md', md: 'lg' }}>
|
||||||
onChange={(newPage) => {
|
<Pagination
|
||||||
load(newPage, 10);
|
value={page}
|
||||||
window.scrollTo(0, 0);
|
onChange={(newPage) => {
|
||||||
}}
|
load(newPage, 10);
|
||||||
total={Math.max(1, totalPages)}
|
window.scrollTo(0, 0);
|
||||||
withEdges
|
}}
|
||||||
radius="md"
|
total={Math.max(1, totalPages)}
|
||||||
/>
|
withEdges
|
||||||
</Center>
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SdgsDesa;
|
export default SdgsDesa;
|
||||||
@@ -204,7 +204,7 @@ function EditAPBDes() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
@@ -215,7 +215,7 @@ function EditAPBDes() {
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: '100%', md: '100%' }}
|
w={{ base: '100%', md: '50%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -368,6 +368,13 @@ function EditAPBDes() {
|
|||||||
{ value: '2', label: 'Level 2 (Sub-kelompok)' },
|
{ value: '2', label: 'Level 2 (Sub-kelompok)' },
|
||||||
{ value: '3', label: 'Level 3 (Detail)' },
|
{ value: '3', label: 'Level 3 (Detail)' },
|
||||||
]}
|
]}
|
||||||
|
styles={{
|
||||||
|
option: {
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
}}
|
||||||
value={String(newItem.level)}
|
value={String(newItem.level)}
|
||||||
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
|
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
|
||||||
/>
|
/>
|
||||||
@@ -378,6 +385,13 @@ function EditAPBDes() {
|
|||||||
{ value: 'belanja', label: 'Belanja' },
|
{ value: 'belanja', label: 'Belanja' },
|
||||||
{ value: 'pembiayaan', label: 'Pembiayaan' },
|
{ value: 'pembiayaan', label: 'Pembiayaan' },
|
||||||
]}
|
]}
|
||||||
|
styles={{
|
||||||
|
option: {
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
}}
|
||||||
value={newItem.tipe}
|
value={newItem.tipe}
|
||||||
onChange={(val) => setNewItem({ ...newItem, tipe: (val as any) || 'pendapatan' })}
|
onChange={(val) => setNewItem({ ...newItem, tipe: (val as any) || 'pendapatan' })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function DetailAPBDes() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -77,7 +77,7 @@ function DetailAPBDes() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: '100%', md: '100%' }}
|
w={{ base: '100%', md: '70%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ function CreateAPBDes() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
@@ -353,6 +353,13 @@ function CreateAPBDes() {
|
|||||||
{ value: '2', label: 'Level 2 (Sub-kelompok)' },
|
{ value: '2', label: 'Level 2 (Sub-kelompok)' },
|
||||||
{ value: '3', label: 'Level 3 (Detail)' },
|
{ value: '3', label: 'Level 3 (Detail)' },
|
||||||
]}
|
]}
|
||||||
|
styles={{
|
||||||
|
option: {
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
}}
|
||||||
value={String(newItem.level)}
|
value={String(newItem.level)}
|
||||||
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
|
onChange={(val) => setNewItem({ ...newItem, level: Number(val) || 1 })}
|
||||||
/>
|
/>
|
||||||
@@ -361,7 +368,15 @@ function CreateAPBDes() {
|
|||||||
data={[
|
data={[
|
||||||
{ value: 'pendapatan', label: 'Pendapatan' },
|
{ value: 'pendapatan', label: 'Pendapatan' },
|
||||||
{ value: 'belanja', label: 'Belanja' },
|
{ value: 'belanja', label: 'Belanja' },
|
||||||
|
{ value: 'pembiayaan', label: 'Pembiayaan' },
|
||||||
]}
|
]}
|
||||||
|
styles={{
|
||||||
|
option: {
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
}}
|
||||||
value={newItem.level === 1 ? null : newItem.tipe}
|
value={newItem.level === 1 ? null : newItem.tipe}
|
||||||
onChange={(val) => setNewItem({ ...newItem, tipe: val as any })}
|
onChange={(val) => setNewItem({ ...newItem, tipe: val as any })}
|
||||||
disabled={newItem.level === 1}
|
disabled={newItem.level === 1}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconFile, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconFile, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -45,82 +45,97 @@ function APBDes() {
|
|||||||
function ListAPBDes({ search }: { search: string }) {
|
function ListAPBDes({ search }: { search: string }) {
|
||||||
const listState = useProxy(apbdes);
|
const listState = useProxy(apbdes);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
|
|
||||||
const { data, page, totalPages, loading, load } = listState.findMany;
|
const { data, page, totalPages, loading, load } = listState.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search);
|
load(page, 10, debouncedSearch);
|
||||||
}, [page, search]);
|
}, [page, debouncedSearch]);
|
||||||
|
|
||||||
const filteredData = data || [];
|
const filteredData = data || [];
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={{ base: 'md', md: 'lg' }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
{/* Desktop Table */}
|
||||||
<Group justify="space-between" mb="md">
|
<Box visibleFrom="md">
|
||||||
<Title order={4}>Daftar APBDes</Title>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Button
|
<Group justify="space-between" mb="md">
|
||||||
leftSection={<IconPlus size={18} />}
|
<Title order={2} size="lg" lh={1.2}>
|
||||||
color="blue"
|
Daftar APBDes
|
||||||
variant="light"
|
</Title>
|
||||||
onClick={() => router.push('/admin/landing-page/apbdes/create')}
|
<Button
|
||||||
>
|
leftSection={<IconPlus size={18} />}
|
||||||
Tambah Baru
|
color="blue"
|
||||||
</Button>
|
variant="light"
|
||||||
</Group>
|
onClick={() => router.push('/admin/landing-page/apbdes/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover miw={0}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '25%' }}>APBDes</TableTh>
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
<TableTh style={{ width: '25%' }}>Tahun</TableTh>
|
APBDes
|
||||||
<TableTh style={{ width: '25%' }}>Dokumen</TableTh>
|
</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Aksi</TableTh>
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
</TableTr>
|
Tahun
|
||||||
</TableThead>
|
</TableTh>
|
||||||
<TableTbody>
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
{filteredData.length > 0 ? (
|
Dokumen
|
||||||
filteredData.map((item) => (
|
</TableTh>
|
||||||
<TableTr key={item.id}>
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
<TableTd style={{ width: '25%' }}>
|
Aksi
|
||||||
<Text fw={500} lineClamp={1}>
|
</TableTh>
|
||||||
APBDes {item.tahun}
|
</TableTr>
|
||||||
</Text>
|
</TableThead>
|
||||||
</TableTd>
|
<TableTbody>
|
||||||
<TableTd style={{ width: '25%' }}>
|
{filteredData.length > 0 ? (
|
||||||
<Text fw={500}>{item.tahun || '-'}</Text>
|
filteredData.map((item) => (
|
||||||
</TableTd>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd>
|
||||||
{item.file?.link ? (
|
<Text fz="md" fw={500} lh={1.5} lineClamp={1}>
|
||||||
<Button
|
APBDes {item.tahun}
|
||||||
component="a"
|
|
||||||
href={item.file.link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
variant="light"
|
|
||||||
leftSection={<IconFile size={16} />}
|
|
||||||
size="xs"
|
|
||||||
radius="sm"
|
|
||||||
>
|
|
||||||
Lihat Dokumen
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Text c="dimmed" fz="sm">
|
|
||||||
Tidak ada dokumen
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
</TableTd>
|
||||||
</TableTd>
|
<TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<Text fz="md" fw={500} lh={1.5}>
|
||||||
<Box w={100}>
|
{item.tahun || '-'}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
{item.file?.link ? (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={item.file.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconFile size={16} />}
|
||||||
|
size="xs"
|
||||||
|
radius="sm"
|
||||||
|
fz="sm"
|
||||||
|
>
|
||||||
|
Lihat Dokumen
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
Tidak ada dokumen
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -128,29 +143,126 @@ function ListAPBDes({ search }: { search: string }) {
|
|||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconDeviceImacCog size={14} />}
|
leftSection={<IconDeviceImacCog size={14} />}
|
||||||
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
||||||
fullWidth
|
fz="sm"
|
||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py="lg">
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
Tidak ada data APBDes yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
)}
|
||||||
) : (
|
</TableTbody>
|
||||||
<TableTr>
|
</Table>
|
||||||
<TableTd colSpan={5}>
|
</Box>
|
||||||
<Center py={20}>
|
</Paper>
|
||||||
<Text color="dimmed">Tidak ada data APBDes yang cocok</Text>
|
</Box>
|
||||||
</Center>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
)}
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Center mt="md">
|
{/* Mobile Cards */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={2} size="lg" lh={1.2}>
|
||||||
|
Daftar APBDes
|
||||||
|
</Title>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/landing-page/apbdes/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Stack gap="md">
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper
|
||||||
|
key={item.id}
|
||||||
|
withBorder
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="md"
|
||||||
|
shadow="sm"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>
|
||||||
|
APBDes {item.tahun}
|
||||||
|
</Text>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm"fw={600} lh={1.4}>
|
||||||
|
Tahun
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4}>
|
||||||
|
{item.tahun || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm"fw={600} lh={1.4}>
|
||||||
|
Dokumen
|
||||||
|
</Text>
|
||||||
|
{item.file?.link ? (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={item.file.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconFile size={14} />}
|
||||||
|
size="xs"
|
||||||
|
radius="sm"
|
||||||
|
fz="xs"
|
||||||
|
lh={1.4}
|
||||||
|
>
|
||||||
|
Lihat
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Text fz="xs" c="dimmed" lh={1.4}>
|
||||||
|
Tidak ada
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={14} />}
|
||||||
|
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
||||||
|
mt="sm"
|
||||||
|
fz="xs"
|
||||||
|
lh={1.4}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Paper withBorder bg={colors['white-1']} p="md" radius="md">
|
||||||
|
<Center py="lg">
|
||||||
|
<Text c="dimmed" fz="xs" lh={1.4}>
|
||||||
|
Tidak ada data APBDes yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Center mt="xl">
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Stack,
|
Stack,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -68,37 +69,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
{/* ✅ Scroll horizontal wrapper */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<Box visibleFrom='md' pb={10}>
|
||||||
<TabsList
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
p="sm"
|
<TabsList
|
||||||
style={{
|
p="sm"
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
style={{
|
||||||
borderRadius: "1rem",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
borderRadius: "1rem",
|
||||||
display: "flex",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
flexWrap: "nowrap",
|
display: "flex",
|
||||||
gap: "0.5rem",
|
flexWrap: "nowrap",
|
||||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
gap: "0.5rem",
|
||||||
}}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
>
|
}}
|
||||||
{tabs.map((tab, i) => (
|
>
|
||||||
<TabsTab
|
{tabs.map((tab, i) => (
|
||||||
key={i}
|
<TabsTab
|
||||||
value={tab.value}
|
key={i}
|
||||||
leftSection={tab.icon}
|
value={tab.value}
|
||||||
style={{
|
leftSection={tab.icon}
|
||||||
fontWeight: 600,
|
style={{
|
||||||
fontSize: "0.9rem",
|
fontWeight: 600,
|
||||||
transition: "all 0.2s ease",
|
fontSize: "0.9rem",
|
||||||
flexShrink: 0, // ✅ mencegah tab mengecil
|
transition: "all 0.2s ease",
|
||||||
}}
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
>
|
}}
|
||||||
{tab.label}
|
>
|
||||||
</TabsTab>
|
{tab.label}
|
||||||
))}
|
</TabsTab>
|
||||||
</TabsList>
|
))}
|
||||||
</ScrollArea>
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box hiddenFrom='md' pb={10}>
|
||||||
|
<ScrollArea
|
||||||
|
type="auto"
|
||||||
|
offsetScrollbars={false}
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
|
||||||
|
<TabsList
|
||||||
|
p="xs" // lebih kecil
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
width: "max-content", // ⬅️ kunci
|
||||||
|
maxWidth: "100%", // ⬅️ penting
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsTab
|
||||||
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
paddingInline: "0.75rem", // ⬅️ lebih ramping
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
key={i}
|
key={i}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default function EditKategoriDesaAntiKorupsi() {
|
|||||||
|
|
||||||
// 🧩 UI
|
// 🧩 UI
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function CreateKategoriDesaAntiKorupsi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconEdit, IconPlus, 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';
|
||||||
@@ -10,9 +27,8 @@ import HeaderSearch from '../../../_com/header';
|
|||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
|
import korupsiState from '../../../_state/landing-page/desa-anti-korupsi';
|
||||||
|
|
||||||
|
|
||||||
function KategoriDesaAntiKorupsi() {
|
function KategoriDesaAntiKorupsi() {
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
@@ -28,126 +44,188 @@ function KategoriDesaAntiKorupsi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListKategoriKegiatan({ search }: { search: string }) {
|
function ListKategoriKegiatan({ search }: { search: string }) {
|
||||||
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi)
|
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
|
||||||
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 router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const { data, page, totalPages, loading, load } = stateKategori.findMany;
|
||||||
data,
|
|
||||||
page,
|
|
||||||
totalPages,
|
|
||||||
loading,
|
|
||||||
load,
|
|
||||||
} = stateKategori.findMany;
|
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
stateKategori.delete.byId(selectedId)
|
stateKategori.delete.byId(selectedId);
|
||||||
setModalHapus(false)
|
setModalHapus(false);
|
||||||
setSelectedId(null)
|
setSelectedId(null);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
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="xl">
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// Mobile cards
|
||||||
<Box py={10}>
|
const renderMobileCards = () => (
|
||||||
<Paper bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Stack gap="md">
|
||||||
<Group justify="space-between" mb="md">
|
{filteredData.length > 0 ? (
|
||||||
<Title order={4}>Daftar Kategori Kegiatan</Title>
|
filteredData.map((item) => (
|
||||||
<Button
|
<Paper key={item.id} p="md" withBorder>
|
||||||
leftSection={<IconPlus size={18} />}
|
<Group justify="space-between" align="flex-start">
|
||||||
color="blue"
|
<Box flex={1}>
|
||||||
variant="light"
|
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45} lineClamp={2}>
|
||||||
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create')}
|
{item.name}
|
||||||
>
|
</Text>
|
||||||
Tambah Baru
|
</Box>
|
||||||
</Button>
|
<Group gap="xs" wrap="nowrap">
|
||||||
</Group>
|
<Button
|
||||||
<Box style={{ overflowX: "auto" }}>
|
variant="light"
|
||||||
<Table highlightOnHover striped verticalSpacing="sm">
|
color="green"
|
||||||
<TableThead>
|
size="xs"
|
||||||
<TableTr>
|
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
|
||||||
<TableTh>Nama Kategori</TableTh>
|
>
|
||||||
<TableTh>Edit</TableTh>
|
<IconEdit size={16} />
|
||||||
<TableTh>Hapus</TableTh>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Paper p="xl" ta="center">
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||||
|
Tidak ada data kategori yang ditemukan
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Desktop table
|
||||||
|
const renderDesktopTable = () => (
|
||||||
|
<Box>
|
||||||
|
<Table highlightOnHover striped verticalSpacing="sm" miw={300}>
|
||||||
|
<TableThead>
|
||||||
|
<TableTr>
|
||||||
|
<TableTh>
|
||||||
|
<Text fw={600} fz="sm" c="dimmed">
|
||||||
|
Nama Kategori
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh>
|
||||||
|
<Text fw={600} fz="sm" c="dimmed">
|
||||||
|
Edit
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh>
|
||||||
|
<Text fw={600} fz="sm" c="dimmed">
|
||||||
|
Hapus
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd>
|
||||||
|
<Text fw={500} fz="md" lh={1.45} lineClamp={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd w={120}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="green"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
|
||||||
|
>
|
||||||
|
<IconEdit size={18} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd w={120}>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={18} />
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
))
|
||||||
<TableTbody>
|
) : (
|
||||||
{filteredData.length > 0 ? (
|
<TableTr>
|
||||||
filteredData.map((item) => (
|
<TableTd colSpan={3} ta="center" py="xl">
|
||||||
<TableTr key={item.id}>
|
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||||
<TableTd>
|
Tidak ada data kategori yang ditemukan
|
||||||
<Box w={200}>
|
</Text>
|
||||||
<Text fw={500} lineClamp={1}>{item.name}</Text>
|
</TableTd>
|
||||||
</Box>
|
</TableTr>
|
||||||
</TableTd>
|
)}
|
||||||
<TableTd>
|
</TableTbody>
|
||||||
<Button
|
</Table>
|
||||||
variant="light"
|
</Box>
|
||||||
color="green"
|
);
|
||||||
size="sm"
|
|
||||||
onClick={() => router.push(`/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/${item.id}`)}
|
return (
|
||||||
>
|
<Box py={{ base: 20, md: 20 }}>
|
||||||
<IconEdit size={18} />
|
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="md" radius="md">
|
||||||
</Button>
|
<Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
|
||||||
</TableTd>
|
<Title order={2} lh={1.2}>
|
||||||
<TableTd>
|
Daftar Kategori Kegiatan
|
||||||
<Button
|
</Title>
|
||||||
variant="light"
|
<Button
|
||||||
color="red"
|
leftSection={<IconPlus size={18} />}
|
||||||
size="sm"
|
color="blue"
|
||||||
onClick={() => {
|
variant="light"
|
||||||
setSelectedId(item.id);
|
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create')}
|
||||||
setModalHapus(true);
|
>
|
||||||
}}
|
Tambah Baru
|
||||||
>
|
</Button>
|
||||||
<IconTrash size={18} />
|
</Group>
|
||||||
</Button>
|
|
||||||
</TableTd>
|
<Box visibleFrom="md">{renderDesktopTable()}</Box>
|
||||||
</TableTr>
|
<Box hiddenFrom="md">{renderMobileCards()}</Box>
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableTr>
|
|
||||||
<TableTd colSpan={2}>
|
|
||||||
<Center py={20}>
|
|
||||||
<Text c="dimmed">Tidak ada data kategori yang ditemukan</Text>
|
|
||||||
</Center>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
)}
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center>
|
|
||||||
<Pagination
|
{totalPages > 1 && (
|
||||||
value={page}
|
<Center mt="xl">
|
||||||
onChange={(newPage) => {
|
<Pagination
|
||||||
load(newPage, 10);
|
value={page}
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
onChange={(newPage) => {
|
||||||
}}
|
load(newPage, 10);
|
||||||
total={totalPages}
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
mt="md"
|
}}
|
||||||
mb="md"
|
total={totalPages}
|
||||||
color="blue"
|
color="blue"
|
||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
{/* Modal Konfirmasi Hapus */}
|
)}
|
||||||
|
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
onClose={() => setModalHapus(false)}
|
onClose={() => setModalHapus(false)}
|
||||||
@@ -158,4 +236,4 @@ function ListKategoriKegiatan({ search }: { search: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KategoriDesaAntiKorupsi
|
export default KategoriDesaAntiKorupsi;
|
||||||
@@ -150,7 +150,7 @@ export default function EditDesaAntiKorupsi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function DetailKegiatanDesa() {
|
|||||||
const data = detailState.findUnique.data;
|
const data = detailState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -53,7 +53,7 @@ export default function DetailKegiatanDesa() {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: "100%", md: "50%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default function CreateDesaAntiKorupsi() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py="md">
|
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||||
<Skeleton height={650} radius="lg" />
|
<Skeleton height={650} radius="lg" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -46,11 +46,13 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box py="md">
|
<Box py={{ base: 'sm', md: 'md' }}>
|
||||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
<Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<Title order={4}>Data Program Desa Anti Korupsi</Title>
|
<Title order={2} lh={1.2}>
|
||||||
<Text c="dimmed" ta="center">
|
Data Program Desa Anti Korupsi
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" ta="center" fz={{ base: 'xs', md: 'sm' }} lh={1.5}>
|
||||||
Belum ada data program yang tersedia
|
Belum ada data program yang tersedia
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -61,48 +63,56 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap="md">
|
<Stack gap={'md'}>
|
||||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
<Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||||
<Title order={4}>Daftar Program Desa Anti Korupsi</Title>
|
<Title order={2} lh={1.2}>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light"
|
Daftar Program Desa Anti Korupsi
|
||||||
onClick={() => router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')}
|
</Title>
|
||||||
>
|
<Button
|
||||||
Tambah Baru
|
leftSection={<IconPlus size={18} />}
|
||||||
</Button>
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() =>
|
||||||
|
router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/create')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
|
{/* Desktop Table */}
|
||||||
|
<Box visibleFrom="md">
|
||||||
<Table
|
<Table
|
||||||
striped
|
striped
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
|
|
||||||
withRowBorders
|
withRowBorders
|
||||||
verticalSpacing="sm"
|
verticalSpacing="sm"
|
||||||
>
|
>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '50%' }}>Nama Program</TableTh>
|
<TableTh w="50%">Nama Program</TableTh>
|
||||||
<TableTh style={{ width: '30%' }}>Kategori</TableTh>
|
<TableTh w="30%">Kategori</TableTh>
|
||||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
<TableTh w="20%" ta="center">
|
||||||
|
Aksi
|
||||||
|
</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '50%' }}>
|
<TableTd w="50%">
|
||||||
<Text fw={500} lineClamp={1}>
|
<Text fw={500} lineClamp={1} fz="md" lh={1.5}>
|
||||||
{item.name || '-'}
|
{item.name || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '30%' }}>
|
<TableTd w="30%">
|
||||||
<Box w={200}>
|
<Text fz="sm" c="dimmed" lineClamp={1} lh={1.5}>
|
||||||
<Text fz="sm" c="dimmed" lineClamp={1}>
|
|
||||||
{item.kategori?.name || '-'}
|
{item.kategori?.name || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%', textAlign: 'center' }}>
|
<TableTd w="20%" ta="center">
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -123,7 +133,7 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
) : (
|
) : (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={3}>
|
<TableTd colSpan={3}>
|
||||||
<Text ta="center" c="dimmed">
|
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
|
||||||
Tidak ditemukan data dengan kata kunci pencarian
|
Tidak ditemukan data dengan kata kunci pencarian
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -132,6 +142,54 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
<Stack gap="xs">
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} p="sm" radius="md" withBorder shadow="xs">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Nama Program</Text>
|
||||||
|
<Text fw={500} fz="sm" lh={1.5} lineClamp={1}>
|
||||||
|
{item.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
|
||||||
|
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1}>
|
||||||
|
{item.kategori?.name || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<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>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Paper p="sm" radius="md" withBorder>
|
||||||
|
<Text ta="center" c="dimmed" fz="xs" lh={1.5}>
|
||||||
|
Tidak ditemukan data dengan kata kunci pencarian
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
@@ -144,7 +202,6 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
}}
|
}}
|
||||||
size="md"
|
size="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
mt="md"
|
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -152,4 +209,4 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DesaAntiKorupsi;
|
export default DesaAntiKorupsi;
|
||||||
@@ -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 { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } 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';
|
||||||
@@ -53,36 +53,41 @@ function LayoutTabsKepuasan({ children }: { children: React.ReactNode }) {
|
|||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* ✅ Scroll horizontal wrapper */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<Box>
|
||||||
<TabsList
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
p="sm"
|
<TabsList
|
||||||
style={{
|
p="sm"
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
style={{
|
||||||
borderRadius: "1rem",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
borderRadius: "1rem",
|
||||||
display: "flex",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
flexWrap: "nowrap",
|
display: "flex",
|
||||||
gap: "0.5rem",
|
flexWrap: "nowrap",
|
||||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
gap: "0.5rem",
|
||||||
}}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
>
|
}}
|
||||||
{tabs.map((e, i) => (
|
>
|
||||||
<TabsTab
|
{tabs.map((tab, i) => (
|
||||||
key={i}
|
<TabsTab
|
||||||
value={e.value}
|
key={i}
|
||||||
leftSection={e.icon}
|
value={tab.value}
|
||||||
style={{
|
leftSection={tab.icon}
|
||||||
fontWeight: 500,
|
style={{
|
||||||
fontSize: "0.9rem",
|
fontWeight: 600,
|
||||||
transition: "all 0.2s ease",
|
fontSize: "0.9rem",
|
||||||
}}
|
transition: "all 0.2s ease",
|
||||||
>
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
{e.label}
|
}}
|
||||||
</TabsTab>
|
>
|
||||||
))}
|
{tab.label}
|
||||||
</TabsList>
|
</TabsTab>
|
||||||
</ScrollArea>
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{tabs.map((e, i) => (
|
{tabs.map((e, i) => (
|
||||||
<TabsPanel key={i} value={e.value}>
|
<TabsPanel key={i} value={e.value}>
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ function EditResponden() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function DetailResponden() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -50,7 +50,7 @@ export default function DetailResponden() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: "100%", md: "60%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py="md">
|
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||||
<Skeleton height={650} radius="lg" />
|
<Skeleton height={650} radius="lg" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -68,11 +68,13 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box py="md">
|
<Box py={{ base: 'md', md: 'lg' }}>
|
||||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
<Paper p={{ base: 'md', md: 'lg' }} radius="lg" shadow="md" withBorder>
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<Title order={4}>Data Responden</Title>
|
<Title order={2} lh={1.2}>
|
||||||
<Text c="dimmed" ta="center">
|
Data Responden
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" ta="center" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||||
Belum ada data responden yang tersedia
|
Belum ada data responden yang tersedia
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -83,12 +85,13 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap="md">
|
<Stack gap={'lg'}>
|
||||||
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
{/* Desktop Table */}
|
||||||
<Title order={4} mb="sm">
|
<Box visibleFrom="md">
|
||||||
Daftar Responden
|
<Paper p="lg" radius="lg" shadow="md" withBorder>
|
||||||
</Title>
|
<Title order={2} size="lg" mb="md" lh={1.2}>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
Daftar Responden
|
||||||
|
</Title>
|
||||||
<Table
|
<Table
|
||||||
striped
|
striped
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
@@ -97,18 +100,18 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
>
|
>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '5%' }}>No</TableTh>
|
<TableTh fz="sm" fw={600} w={60}>No</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Nama</TableTh>
|
<TableTh fz="sm" fw={600}>Nama</TableTh>
|
||||||
<TableTh style={{ width: '20%' }}>Tanggal</TableTh>
|
<TableTh fz="sm" fw={600}>Tanggal</TableTh>
|
||||||
<TableTh style={{ width: '20%' }}>Jenis Kelamin</TableTh>
|
<TableTh fz="sm" fw={600}>Jenis Kelamin</TableTh>
|
||||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
<TableTh fz="sm" fw={600} w={120}>Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length === 0 ? (
|
{filteredData.length === 0 ? (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={5}>
|
<TableTd colSpan={5}>
|
||||||
<Text ta="center" c="dimmed">
|
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
|
||||||
Tidak ditemukan data dengan kata kunci pencarian
|
Tidak ditemukan data dengan kata kunci pencarian
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -116,24 +119,18 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
) : (
|
) : (
|
||||||
filteredData.map((item, index) => (
|
filteredData.map((item, index) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>{index + 1}</TableTd>
|
<TableTd fz="md" lh={1.5}>{index + 1}</TableTd>
|
||||||
<TableTd>{item.name}</TableTd>
|
<TableTd fz="md" lh={1.5}>{item.name}</TableTd>
|
||||||
<TableTd>
|
<TableTd fz="md" lh={1.5}>
|
||||||
<Box w={150}>
|
|
||||||
{item.tanggal
|
{item.tanggal
|
||||||
? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})
|
})
|
||||||
: '-'}
|
: '-'}
|
||||||
</Box>
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Box w={100}>
|
|
||||||
{item.jenisKelamin.name}
|
|
||||||
</Box>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
|
<TableTd fz="md" lh={1.5}>{item.jenisKelamin.name}</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -155,8 +152,64 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
)}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Paper>
|
||||||
</Paper>
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Title order={2} size="md" lh={1.2} px="md">
|
||||||
|
Daftar Responden
|
||||||
|
</Title>
|
||||||
|
{filteredData.length === 0 ? (
|
||||||
|
<Paper p="md" radius="lg" shadow="sm" mx="md">
|
||||||
|
<Text ta="center" c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
Tidak ditemukan data dengan kata kunci pencarian
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} p="md" radius="lg" shadow="sm" mx="md">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>Nama</Text>
|
||||||
|
<Text fz="md" lh={1.5}>{item.name}</Text>
|
||||||
|
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>Tanggal</Text>
|
||||||
|
<Text fz="md" lh={1.5}>
|
||||||
|
{item.tanggal
|
||||||
|
? new Date(item.tanggal).toLocaleDateString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
: '-'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>Jenis Kelamin</Text>
|
||||||
|
<Text fz="md" lh={1.5}>{item.jenisKelamin.name}</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImac size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/landing-page/indeks-kepuasan-masyarakat/responden/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mt="xs"
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
@@ -167,7 +220,7 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
}}
|
}}
|
||||||
size="md"
|
size="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
mt="md"
|
mt={{ base: 'md', md: 'lg' }}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -175,4 +228,4 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Responden;
|
export default Responden;
|
||||||
@@ -56,6 +56,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
|
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
<TabsList
|
<TabsList
|
||||||
p="sm"
|
p="sm"
|
||||||
@@ -63,6 +64,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "inset 0 0 10px 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
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
@@ -74,6 +79,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: "0.9rem",
|
fontSize: "0.9rem",
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function EditKategoriPrestasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function CreateKategoriPrestasi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconEdit, IconPlus, 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';
|
||||||
@@ -32,6 +32,7 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
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 router = useRouter()
|
const router = useRouter()
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||||
|
|
||||||
const handleHapus = () => {
|
const handleHapus = () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
@@ -50,14 +51,14 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
} = stateKategori.findMany
|
} = stateKategori.findMany
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, debouncedSearch)
|
||||||
}, [page, search])
|
}, [page, debouncedSearch])
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || []
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py="md">
|
||||||
<Skeleton h={500} />
|
<Skeleton h={500} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
@@ -65,28 +66,33 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
{/* DESKTOP: Table */}
|
||||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm" withBorder>
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm" withBorder>
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="xl">
|
||||||
<Title order={4} c="dark">List Kategori Prestasi</Title>
|
<Title order={2} size="lg" lh={1.2}>List Kategori Prestasi</Title>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}>
|
<Button
|
||||||
Tambah Baru
|
leftSection={<IconPlus size={18} />}
|
||||||
</Button>
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<Box visibleFrom="md">
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
<Table verticalSpacing="sm" highlightOnHover>
|
<Table verticalSpacing="sm" highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama Kategori</TableTh>
|
<TableTh><Text fz="sm" fw={600} c="dark">Nama Kategori</Text></TableTh>
|
||||||
<TableTh style={{ width: '120px' }} ta={'center'}>Edit</TableTh>
|
<TableTh w={120} ta="center"><Text fz="sm" fw={600} c="dark">Edit</Text></TableTh>
|
||||||
<TableTh ta={'center'} style={{ width: '120px' }}>Delete</TableTh>
|
<TableTh w={120} ta="center"><Text fz="sm" fw={600} c="dark">Delete</Text></TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length === 0 ? (
|
{filteredData.length === 0 ? (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={2} style={{ textAlign: 'center' }}>
|
<TableTd colSpan={3} ta="center">
|
||||||
<Text py="md" c="dimmed">
|
<Text py="md" c="dimmed" fz="sm" lh={1.5}>
|
||||||
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -95,68 +101,130 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={200}>
|
<Text truncate="end" fz="md" lh={1.5} c="dark">
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
{item.name}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ textAlign: 'center', width: '120px' }}>
|
<TableTd ta="center" w={120}>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
||||||
>
|
>
|
||||||
<IconEdit size={18} />
|
<IconEdit size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ textAlign: 'center', width: '120px' }}>
|
<TableTd ta="center" w={120}>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedId(item.id);
|
setSelectedId(item.id);
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconTrash size={18} />
|
<IconTrash size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Center mt="xl">
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage)}
|
||||||
|
total={totalPages}
|
||||||
|
withEdges
|
||||||
|
size="sm"
|
||||||
|
styles={{
|
||||||
|
control: {
|
||||||
|
'&[data-active]': {
|
||||||
|
background: `${colors['blue-button']} !important`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{/* MOBILE: Card */}
|
||||||
<Center mt="lg">
|
<Box hiddenFrom="md">
|
||||||
<Pagination
|
<Stack gap="md">
|
||||||
value={page}
|
{filteredData.length === 0 ? (
|
||||||
onChange={(newPage) => load(newPage)}
|
<Paper p="lg" ta="center">
|
||||||
total={totalPages}
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
withEdges
|
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
||||||
size="sm"
|
</Text>
|
||||||
styles={{
|
</Paper>
|
||||||
control: {
|
) : (
|
||||||
'&[data-active]': {
|
filteredData.map((item) => (
|
||||||
background: `${colors['blue-button']} !important`,
|
<Paper key={item.id} p="md" withBorder bg={colors['white-1']}>
|
||||||
},
|
<Stack gap="xs">
|
||||||
},
|
<Text fz="sm" lh={1.5} fw={600} c="dark">{item.name}</Text>
|
||||||
}}
|
<Group justify="flex-end" gap="xs">
|
||||||
/>
|
<Button
|
||||||
</Center>
|
variant="light"
|
||||||
)}
|
color="green"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
||||||
|
>
|
||||||
|
<IconEdit size={14} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={14} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Center py="lg">
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage)}
|
||||||
|
total={totalPages}
|
||||||
|
withEdges
|
||||||
|
size="xs"
|
||||||
|
styles={{
|
||||||
|
control: {
|
||||||
|
'&[data-active]': {
|
||||||
|
background: `${colors['blue-button']} !important`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Modal Konfirmasi Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
{/* Modal Konfirmasi Hapus */}
|
</Box>
|
||||||
<ModalKonfirmasiHapus
|
|
||||||
opened={modalHapus}
|
|
||||||
onClose={() => setModalHapus(false)}
|
|
||||||
onConfirm={handleHapus}
|
|
||||||
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
|
|
||||||
/>
|
|
||||||
</Box >
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KategoriPrestasiDesa
|
export default KategoriPrestasiDesa
|
||||||
@@ -128,7 +128,7 @@ export default function EditPrestasiDesa() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function DetailPrestasiDesa() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -53,7 +53,7 @@ function DetailPrestasiDesa() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: "100%", md: "60%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function CreatePrestasiDesa() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useDebouncedValue, useMediaQuery, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconPlus, 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';
|
||||||
@@ -28,6 +28,8 @@ function ListPrestasiDesa() {
|
|||||||
function ListPrestasi({ search }: { search: string }) {
|
function ListPrestasi({ search }: { search: string }) {
|
||||||
const listState = useProxy(prestasiState.prestasiDesa)
|
const listState = useProxy(prestasiState.prestasiDesa)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -38,93 +40,149 @@ function ListPrestasi({ search }: { search: string }) {
|
|||||||
} = listState.findMany
|
} = listState.findMany
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search);
|
load(page, 10, debouncedSearch);
|
||||||
}, [page, search]);
|
}, [page, debouncedSearch]);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || []
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={{ base: 'sm', md: 'md' }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||||
<Title order={4}>Daftar Prestasi Desa</Title>
|
<Title order={2} size={isMobile ? 'md' : 'lg'} lh={1.2}>
|
||||||
|
Daftar Prestasi Desa
|
||||||
|
</Title>
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa/create')}
|
onClick={() => router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa/create')}
|
||||||
|
size={isMobile ? 'xs' : 'sm'}
|
||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
<Table highlightOnHover>
|
{/* Desktop Table */}
|
||||||
|
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||||
|
<Table highlightOnHover striped verticalSpacing="sm" miw={800}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '25%' }}>Nama Prestasi</TableTh>
|
<TableTh>Nama Prestasi</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Deskripsi</TableTh>
|
<TableTh>Deskripsi</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
|
<TableTh>Kategori</TableTh>
|
||||||
<TableTh style={{ width: '25%', textAlign: 'center' }}>Aksi</TableTh>
|
<TableTh ta="center">Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd style={{ maxWidth: 250 }}>
|
||||||
<Box w={100}>
|
<Text truncate="end" fz="md" lh={1.5}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
{item.name}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd style={{ maxWidth: 250 }}>
|
||||||
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
<Text lineClamp={1} fz="md" c="dimmed" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd>
|
||||||
<Box w={150}>
|
<Text truncate="end" fz="md" lh={1.5}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text>
|
{item.kategori?.name || 'Tidak ada kategori'}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%', textAlign: 'center' }}>
|
<TableTd ta="center">
|
||||||
<Button
|
<Center>
|
||||||
size="xs"
|
<Button
|
||||||
radius="md"
|
size="sm"
|
||||||
variant="light"
|
radius="md"
|
||||||
color="blue"
|
variant="light"
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
color="blue"
|
||||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
>
|
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
|
||||||
Detail
|
>
|
||||||
</Button>
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={4} style={{ textAlign: 'center' }}>
|
<TableTd colSpan={4} ta="center">
|
||||||
<Text c="dimmed" py="md">Tidak ada data prestasi</Text>
|
<Text c="dimmed" py="md" fz="sm" lh={1.4}>
|
||||||
|
Tidak ada data prestasi
|
||||||
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
)}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<Stack hiddenFrom="md" gap="xs">
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} withBorder p="sm" radius="sm">
|
||||||
|
<Stack gap={"xs"}>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Nama Prestasi</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4} lineClamp={2}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.5} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Kategori</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4}>
|
||||||
|
{item.kategori?.name || 'Tidak ada kategori'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Group justify="flex-end" mt="xs">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={14} />}
|
||||||
|
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Center py="md">
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||||
|
Tidak ada data prestasi
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<Center mt="lg">
|
<Center mt={{ base: 'md', md: 'lg' }}>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={load}
|
onChange={load}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
withEdges
|
withEdges
|
||||||
size="sm"
|
size={isMobile ? 'xs' : 'sm'}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
@@ -132,4 +190,4 @@ function ListPrestasi({ search }: { search: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ListPrestasiDesa;
|
export default ListPrestasiDesa;
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Stack,
|
Stack,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -74,36 +75,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
{/* ✅ Scroll horizontal wrapper */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<Box visibleFrom='md' pb={10}>
|
||||||
<TabsList
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
p="sm"
|
<TabsList
|
||||||
style={{
|
p="sm"
|
||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
style={{
|
||||||
borderRadius: "1rem",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
borderRadius: "1rem",
|
||||||
display: "flex",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
flexWrap: "nowrap",
|
display: "flex",
|
||||||
gap: "0.5rem",
|
flexWrap: "nowrap",
|
||||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
gap: "0.5rem",
|
||||||
}}
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsTab
|
||||||
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box hiddenFrom='md' pb={10}>
|
||||||
|
<ScrollArea
|
||||||
|
type="auto"
|
||||||
|
offsetScrollbars={false}
|
||||||
|
w="100%"
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
|
||||||
<TabsTab
|
<TabsList
|
||||||
key={i}
|
p="xs" // lebih kecil
|
||||||
value={tab.value}
|
style={{
|
||||||
leftSection={tab.icon}
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
style={{
|
borderRadius: "1rem",
|
||||||
fontWeight: 600,
|
display: "flex",
|
||||||
fontSize: "0.9rem",
|
flexWrap: "nowrap",
|
||||||
transition: "all 0.2s ease",
|
gap: "0.5rem",
|
||||||
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
width: "max-content", // ⬅️ kunci
|
||||||
}}
|
maxWidth: "100%", // ⬅️ penting
|
||||||
>
|
}}
|
||||||
{tab.label}
|
>
|
||||||
</TabsTab>
|
{tabs.map((tab, i) => (
|
||||||
))}
|
<TabsTab
|
||||||
</TabsList>
|
key={i}
|
||||||
</ScrollArea>
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
paddingInline: "0.75rem", // ⬅️ lebih ramping
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
|
|||||||
12
src/app/admin/(dashboard)/landing-page/profil/_lib/sosmed.ts
Normal file
12
src/app/admin/(dashboard)/landing-page/profil/_lib/sosmed.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const sosmedMap = {
|
||||||
|
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
||||||
|
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
||||||
|
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
||||||
|
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
||||||
|
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
||||||
|
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
||||||
|
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
||||||
|
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
||||||
|
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
||||||
|
custom: { label: 'Custom Icon', src: null },
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import SelectSocialMediaEdit from '@/app/admin/(dashboard)/_com/selectSocialMediaEdit';
|
||||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
@@ -14,7 +16,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Loader
|
Loader,
|
||||||
} from '@mantine/core';
|
} 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';
|
||||||
@@ -23,15 +25,45 @@ 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';
|
||||||
|
|
||||||
|
type SosmedKey =
|
||||||
|
| 'none'
|
||||||
|
| 'facebook'
|
||||||
|
| 'instagram'
|
||||||
|
| 'tiktok'
|
||||||
|
| 'youtube'
|
||||||
|
| 'whatsapp'
|
||||||
|
| 'gmail'
|
||||||
|
| 'telegram'
|
||||||
|
| 'x'
|
||||||
|
| 'telephone'
|
||||||
|
| 'custom';
|
||||||
|
|
||||||
|
const sosmedMap: Record<SosmedKey, { label: string; src: string | null }> = {
|
||||||
|
none: { label: "None", src: '/no-image.jpg' },
|
||||||
|
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
||||||
|
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
||||||
|
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
||||||
|
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
||||||
|
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
||||||
|
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
||||||
|
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
||||||
|
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
||||||
|
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
||||||
|
custom: { label: 'Custom Icon', src: null },
|
||||||
|
};
|
||||||
|
|
||||||
function EditMediaSosial() {
|
function EditMediaSosial() {
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
|
const [selectedSosmed, setSelectedSosmed] = useState<SosmedKey>('facebook');
|
||||||
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({
|
||||||
name: '',
|
name: '',
|
||||||
|
icon: '',
|
||||||
iconUrl: '',
|
iconUrl: '',
|
||||||
imageId: '',
|
imageId: '',
|
||||||
});
|
});
|
||||||
@@ -39,13 +71,14 @@ function EditMediaSosial() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState({
|
||||||
name: "",
|
name: '',
|
||||||
iconUrl: "",
|
icon: '',
|
||||||
imageId: "",
|
iconUrl: '',
|
||||||
imageUrl: "",
|
imageId: '',
|
||||||
|
imageUrl: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load data by ID
|
// Load Data by ID
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -54,81 +87,97 @@ function EditMediaSosial() {
|
|||||||
try {
|
try {
|
||||||
const data = await stateMediaSosial.update.load(id);
|
const data = await stateMediaSosial.update.load(id);
|
||||||
|
|
||||||
if (data) {
|
if (!data) return;
|
||||||
// isi form awal
|
|
||||||
const newForm = {
|
|
||||||
name: data.name || "",
|
|
||||||
iconUrl: data.iconUrl || "",
|
|
||||||
imageId: data.imageId || "",
|
|
||||||
};
|
|
||||||
setFormData(newForm);
|
|
||||||
|
|
||||||
// simpan juga versi original
|
// Tentukan default/custom icon
|
||||||
setOriginalData({
|
// Tentukan default/custom icon
|
||||||
...newForm,
|
if (data.imageId) {
|
||||||
imageUrl: data.image?.link || "",
|
setSelectedSosmed('custom');
|
||||||
});
|
} else {
|
||||||
|
// ✅ Gunakan langsung data.icon jika ada dan valid
|
||||||
setPreviewImage(data.image?.link || null);
|
if (data.icon && sosmedMap[data.icon as SosmedKey]) {
|
||||||
|
setSelectedSosmed(data.icon as SosmedKey);
|
||||||
|
} else {
|
||||||
|
setSelectedSosmed('none'); // fallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading media sosial:', error);
|
const newForm = {
|
||||||
toast.error(
|
name: data.name || '',
|
||||||
error instanceof Error ? error.message : 'Gagal mengambil data media sosial'
|
icon: data.icon || '',
|
||||||
);
|
iconUrl: data.iconUrl || '',
|
||||||
|
imageId: data.imageId || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
setFormData(newForm);
|
||||||
|
|
||||||
|
setOriginalData({
|
||||||
|
...newForm,
|
||||||
|
imageUrl: data.image?.link || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
setPreviewImage(data.image?.link || null);
|
||||||
|
} catch {
|
||||||
|
toast.error('Gagal mengambil data media sosial');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleChange = (field: string, value: string) => {
|
const handleChange = (field: keyof typeof formData, value: string) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// update global state hanya saat submit
|
|
||||||
stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
|
stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
|
||||||
|
|
||||||
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) return toast.error('Gagal upload gambar');
|
if (!uploaded?.id) {
|
||||||
|
toast.error('Gagal upload gambar');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
stateMediaSosial.update.form.imageId = uploaded.id;
|
stateMediaSosial.update.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🚨 Tambahkan ini untuk debugging
|
||||||
|
console.log("Data yang akan dikirim ke backend:", stateMediaSosial.update.form);
|
||||||
|
|
||||||
await stateMediaSosial.update.update();
|
await stateMediaSosial.update.update();
|
||||||
toast.success('Media sosial berhasil diperbarui!');
|
toast.success('Media sosial berhasil diperbarui!');
|
||||||
router.push('/admin/landing-page/profil/media-sosial');
|
router.push('/admin/landing-page/profil/media-sosial');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating media sosial:', error);
|
console.error("Error di handleSubmit:", error); // 🚨 Tambahkan ini juga
|
||||||
toast.error('Terjadi kesalahan saat memperbarui media sosial');
|
toast.error('Terjadi kesalahan saat memperbarui media sosial');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ Tombol Batal → balikin ke data original
|
|
||||||
const handleResetForm = () => {
|
const handleResetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: originalData.name,
|
name: originalData.name,
|
||||||
|
icon: originalData.icon,
|
||||||
iconUrl: originalData.iconUrl,
|
iconUrl: originalData.iconUrl,
|
||||||
imageId: originalData.imageId,
|
imageId: originalData.imageId,
|
||||||
});
|
});
|
||||||
setPreviewImage(originalData.imageUrl || null);
|
setPreviewImage(originalData.imageUrl || null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
toast.info("Form dikembalikan ke data awal");
|
toast.info('Form dikembalikan ke data awal');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
px={{ base: 'sm', md: 'lg' }}
|
|
||||||
py="md"
|
|
||||||
>
|
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
@@ -147,80 +196,119 @@ function EditMediaSosial() {
|
|||||||
style={{ border: '1px solid #e0e0e0' }}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Upload Gambar */}
|
{/* Upload / Icon */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
Gambar Program Inovasi
|
Icon / Gambar Media Sosial
|
||||||
</Text>
|
</Text>
|
||||||
<Dropzone
|
|
||||||
onDrop={(files) => {
|
{/* Custom Upload */}
|
||||||
const selectedFile = files[0];
|
{/* PILIH ICON */}
|
||||||
if (selectedFile) {
|
<SelectSocialMediaEdit
|
||||||
setFile(selectedFile);
|
value={selectedSosmed}
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
onChange={(key) => {
|
||||||
|
setSelectedSosmed(key);
|
||||||
|
|
||||||
|
if (key === 'custom') {
|
||||||
|
// custom → gunakan Dropzone
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
icon: '',
|
||||||
|
imageId: '',
|
||||||
|
}));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// default → pakai icon bawaan
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
icon: key, // <-- simpan 'facebook', bukan path
|
||||||
|
imageId: '',
|
||||||
|
}));
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
/>
|
||||||
maxSize={5 * 1024 ** 2}
|
|
||||||
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
|
||||||
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 .png, .jpg, .jpeg, webp
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{/* ✅ Preview gambar + tombol X */}
|
{selectedSosmed === 'custom' ? (
|
||||||
{previewImage && (
|
<>
|
||||||
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
<Dropzone
|
||||||
<Image
|
onDrop={(files) => {
|
||||||
src={previewImage}
|
const selectedFile = files[0];
|
||||||
alt="Preview Gambar"
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
|
handleChange('imageId', '');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onReject={() => toast.error('File tidak valid')}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
radius="md"
|
radius="md"
|
||||||
style={{
|
p="xl"
|
||||||
maxHeight: 200,
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Tombol hapus (pojok kanan atas) */}
|
|
||||||
<ActionIcon
|
|
||||||
variant="filled"
|
|
||||||
color="red"
|
|
||||||
radius="xl"
|
|
||||||
size="sm"
|
|
||||||
pos="absolute"
|
|
||||||
top={5}
|
|
||||||
right={5}
|
|
||||||
onClick={() => {
|
|
||||||
setPreviewImage(null);
|
|
||||||
setFile(null);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<IconX size={14} />
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
</ActionIcon>
|
<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 align="center" gap="xs">
|
||||||
|
<Text fw={500}>Seret gambar atau klik untuk pilih</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Maksimal 5MB, format: .png, .jpg, .jpeg, .webp
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 200,
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setFile(null);
|
||||||
|
setPreviewImage(null);
|
||||||
|
handleChange('imageId', '');
|
||||||
|
}}
|
||||||
|
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Default icon
|
||||||
|
<Box mt="xs">
|
||||||
|
<Image
|
||||||
|
src={sosmedMap[selectedSosmed].src || ''}
|
||||||
|
alt="Icon bawaan"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
radius="md"
|
||||||
|
style={{ border: '1px solid #ddd', padding: 4, background: '#fff' }}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -237,25 +325,17 @@ function EditMediaSosial() {
|
|||||||
{/* Link Media Sosial */}
|
{/* Link Media Sosial */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Link Media Sosial / Nomor Telepon"
|
label="Link Media Sosial / Nomor Telepon"
|
||||||
placeholder="Masukkan link media sosial atau nomor telepon"
|
placeholder="Masukkan link atau nomor telepon"
|
||||||
value={formData.iconUrl}
|
value={formData.iconUrl}
|
||||||
onChange={(e) => handleChange('iconUrl', e.target.value)}
|
onChange={(e) => handleChange('iconUrl', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
{/* Tombol Batal */}
|
<Button variant="outline" color="gray" radius="md" size="md" onClick={handleResetForm}>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
color="gray"
|
|
||||||
radius="md"
|
|
||||||
size="md"
|
|
||||||
onClick={handleResetForm}
|
|
||||||
>
|
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Tombol Simpan */}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'use client'
|
'use client'
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||||
@@ -8,6 +9,7 @@ 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';
|
||||||
|
import { sosmedMap } from '../../_lib/sosmed';
|
||||||
|
|
||||||
function DetailMediaSosial() {
|
function DetailMediaSosial() {
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
@@ -16,6 +18,14 @@ function DetailMediaSosial() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const getIconSource = (item: any) => {
|
||||||
|
if (item.image?.link) return item.image.link;
|
||||||
|
if (item.icon && sosmedMap[item.icon as keyof typeof sosmedMap]?.src) {
|
||||||
|
return sosmedMap[item.icon as keyof typeof sosmedMap].src;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
stateMediaSosial.findUnique.load(params?.id as string);
|
stateMediaSosial.findUnique.load(params?.id as string);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -40,7 +50,7 @@ function DetailMediaSosial() {
|
|||||||
const data = stateMediaSosial.findUnique.data;
|
const data = stateMediaSosial.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -52,7 +62,7 @@ function DetailMediaSosial() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: "100%", md: "50%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -77,46 +87,47 @@ function DetailMediaSosial() {
|
|||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold">Gambar</Text>
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
{data.image?.link ? (
|
{(() => {
|
||||||
<Image
|
const src = getIconSource(data);
|
||||||
src={data.image.link}
|
|
||||||
alt={data.name || 'Gambar Media Sosial'}
|
|
||||||
w="100%"
|
|
||||||
maw={120} // max width biar tidak keluar layar
|
|
||||||
h="auto"
|
|
||||||
radius="md"
|
|
||||||
fit="cover"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
|
|
||||||
) : (
|
if (src) {
|
||||||
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
return (
|
||||||
)}
|
<Image
|
||||||
|
loading="lazy"
|
||||||
|
src={src}
|
||||||
|
alt={data.name}
|
||||||
|
fit={data.image?.link ? "cover" : "contain"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
||||||
|
})()}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedId(data.id);
|
setSelectedId(data.id);
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}}
|
}}
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() => router.push(`/admin/landing-page/profil/media-sosial/${data.id}/edit`)}
|
onClick={() => router.push(`/admin/landing-page/profil/media-sosial/${data.id}/edit`)}
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* 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 {
|
import {
|
||||||
@@ -22,10 +23,40 @@ 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 profileLandingPageState from '../../../../_state/landing-page/profile';
|
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
||||||
|
import SelectSosialMedia from '@/app/admin/(dashboard)/_com/selectSocialMedia';
|
||||||
|
|
||||||
|
// ⭐ Tambah type SosmedKey
|
||||||
|
type SosmedKey =
|
||||||
|
| 'facebook'
|
||||||
|
| 'instagram'
|
||||||
|
| 'tiktok'
|
||||||
|
| 'youtube'
|
||||||
|
| 'whatsapp'
|
||||||
|
| 'gmail'
|
||||||
|
| 'telegram'
|
||||||
|
| 'x'
|
||||||
|
| 'telephone'
|
||||||
|
| 'custom';
|
||||||
|
|
||||||
|
// ⭐ mapping icon sosmed bawaan
|
||||||
|
const sosmedMap: Record<SosmedKey, { label: string; src: string | null }> = {
|
||||||
|
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
||||||
|
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
||||||
|
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
||||||
|
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
||||||
|
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
||||||
|
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
||||||
|
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
||||||
|
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
||||||
|
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
||||||
|
custom: { label: 'Custom Icon', src: null },
|
||||||
|
};
|
||||||
|
|
||||||
export default function CreateMediaSosial() {
|
export default function CreateMediaSosial() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
|
|
||||||
|
const [selectedSosmed, setSelectedSosmed] = useState<SosmedKey>('facebook');
|
||||||
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 [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@@ -39,16 +70,33 @@ export default function CreateMediaSosial() {
|
|||||||
name: '',
|
name: '',
|
||||||
imageId: '',
|
imageId: '',
|
||||||
iconUrl: '',
|
iconUrl: '',
|
||||||
|
icon: ''
|
||||||
};
|
};
|
||||||
setPreviewImage(null);
|
|
||||||
setFile(null);
|
setFile(null);
|
||||||
|
setPreviewImage(null);
|
||||||
|
setSelectedSosmed('facebook');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// ──────────────── ⭐ CASE 1: PAKAI ICON DEFAULT ────────────────
|
||||||
|
if (selectedSosmed !== 'custom') {
|
||||||
|
stateMediaSosial.create.form.imageId = null;
|
||||||
|
stateMediaSosial.create.form.icon = sosmedMap[selectedSosmed].src!;
|
||||||
|
|
||||||
|
await stateMediaSosial.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/landing-page/profil/media-sosial');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────── ⭐ CASE 2: CUSTOM ICON → WAJIB UPLOAD ────────────────
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
toast.warn('Silakan upload icon custom terlebih dahulu');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
@@ -59,10 +107,12 @@ export default function CreateMediaSosial() {
|
|||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
toast.error('Gagal mengunggah icon custom');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
stateMediaSosial.create.form.imageId = uploaded.id;
|
stateMediaSosial.create.form.imageId = uploaded.id;
|
||||||
|
stateMediaSosial.create.form.icon = null;
|
||||||
|
|
||||||
await stateMediaSosial.create.create();
|
await stateMediaSosial.create.create();
|
||||||
|
|
||||||
@@ -77,12 +127,13 @@ export default function CreateMediaSosial() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={2} ml="sm" c="dark" lh={1.2} fz={{ base: 'md', md: 'lg' }}>
|
||||||
Tambah Media Sosial
|
Tambah Media Sosial
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -96,112 +147,120 @@ export default function CreateMediaSosial() {
|
|||||||
style={{ border: '1px solid #e0e0e0' }}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Box>
|
{/* Select Sosmed */}
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
<SelectSosialMedia value={selectedSosmed} onChange={setSelectedSosmed} />
|
||||||
Gambar Program Inovasi
|
|
||||||
</Text>
|
|
||||||
<Dropzone
|
|
||||||
onDrop={(files) => {
|
|
||||||
const selectedFile = files[0];
|
|
||||||
if (selectedFile) {
|
|
||||||
setFile(selectedFile);
|
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
|
||||||
maxSize={5 * 1024 ** 2}
|
|
||||||
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
|
||||||
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 .png, .jpg, .jpeg, webp
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{/* ✅ Preview gambar + tombol X */}
|
{/* Custom icon uploader */}
|
||||||
{previewImage && (
|
{selectedSosmed === 'custom' && (
|
||||||
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
<Box>
|
||||||
<Image
|
<Text fw="bold" fz={{ base: 'sm', md: 'md' }} lh={1.45} mb={6}>
|
||||||
src={previewImage}
|
Upload Custom Icon
|
||||||
alt="Preview Gambar"
|
</Text>
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
maxHeight: 200,
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
}}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Tombol hapus (pojok kanan atas) */}
|
<Dropzone
|
||||||
<ActionIcon
|
onDrop={(files) => {
|
||||||
variant="filled"
|
const selectedFile = files[0];
|
||||||
color="red"
|
if (selectedFile) {
|
||||||
radius="xl"
|
setFile(selectedFile);
|
||||||
size="sm"
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
pos="absolute"
|
}
|
||||||
top={5}
|
}}
|
||||||
right={5}
|
onReject={() => toast.error('File tidak valid')}
|
||||||
onClick={() => {
|
maxSize={5 * 1024 ** 2}
|
||||||
setPreviewImage(null);
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
setFile(null);
|
radius="md"
|
||||||
}}
|
p="xl"
|
||||||
style={{
|
>
|
||||||
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
<Group justify="center" gap="xl" mih={180}>
|
||||||
}}
|
<Dropzone.Accept>
|
||||||
>
|
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
||||||
<IconX size={14} />
|
</Dropzone.Accept>
|
||||||
</ActionIcon>
|
<Dropzone.Reject>
|
||||||
</Box>
|
<IconX size={48} color="red" stroke={1.5} />
|
||||||
)}
|
</Dropzone.Reject>
|
||||||
</Box>
|
<Dropzone.Idle>
|
||||||
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
|
</Dropzone.Idle>
|
||||||
|
|
||||||
|
<Stack align="center" gap="xs">
|
||||||
|
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
Seret gambar atau klik untuk pilih
|
||||||
|
</Text>
|
||||||
|
<Text fz={{ base: 12, md: 'sm' }} c="dimmed" lh={1.4}>
|
||||||
|
Maksimal 5MB, format .png, .jpg, .jpeg, webp
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
{previewImage && (
|
||||||
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={previewImage}
|
||||||
|
alt="Preview"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
maxHeight: 200,
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setFile(null);
|
||||||
|
setPreviewImage(null);
|
||||||
|
}}
|
||||||
|
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Input name */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Nama Media Sosial / Kontak"
|
label={
|
||||||
placeholder="Masukkan nama media sosial atau kontak"
|
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
value={stateMediaSosial.create.form.name || ''}
|
Nama Media Sosial
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
placeholder="Masukkan nama media sosial"
|
||||||
|
value={stateMediaSosial.create.form.name ?? ''}
|
||||||
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
|
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Input link */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Link Media Sosial / Nomor Telepon"
|
label={
|
||||||
placeholder="Masukkan link media sosial atau nomor telepon"
|
<Text fw={500} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
value={stateMediaSosial.create.form.iconUrl || ''}
|
Link / Kontak
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
placeholder="Masukkan link atau nomor"
|
||||||
|
value={stateMediaSosial.create.form.iconUrl ?? ''}
|
||||||
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
|
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
<Button
|
<Button variant="outline" color="gray" radius="md" onClick={resetForm}>
|
||||||
variant="outline"
|
|
||||||
color="gray"
|
|
||||||
radius="md"
|
|
||||||
size="md"
|
|
||||||
onClick={resetForm}
|
|
||||||
>
|
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
onClick={handleSubmit}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
@@ -215,4 +274,4 @@ export default function CreateMediaSosial() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,33 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import {
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconPlus, 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 profileLandingPageState from '../../../_state/landing-page/profile';
|
import profileLandingPageState from '../../../_state/landing-page/profile';
|
||||||
|
import { sosmedMap } from '../_lib/sosmed';
|
||||||
|
|
||||||
function MediaSosial() {
|
function MediaSosial() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -26,8 +46,17 @@ function MediaSosial() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListMediaSosial({ search }: { search: string }) {
|
function ListMediaSosial({ search }: { search: string }) {
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||||
|
|
||||||
|
const getIconSource = (item: any) => {
|
||||||
|
if (item.image?.link) return item.image.link;
|
||||||
|
if (item.icon && sosmedMap[item.icon as keyof typeof sosmedMap]?.src) {
|
||||||
|
return sosmedMap[item.icon as keyof typeof sosmedMap].src;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -38,88 +67,204 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
} = stateMediaSosial.findMany;
|
} = stateMediaSosial.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, debouncedSearch);
|
||||||
}, [page, search])
|
}, [page, debouncedSearch]);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || [];
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={{ base: 'sm', sm: 'md' }}>
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={{ base: 'sm', sm: 'md' }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', sm: 'lg' }} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb={{ base: 'sm', sm: 'md' }}>
|
||||||
<Title order={4}>Daftar Media Sosial</Title>
|
<Title order={4} lh={1.15}>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profil/media-sosial/create')}>
|
Daftar Media Sosial
|
||||||
Tambah Baru
|
</Title>
|
||||||
</Button>
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/landing-page/profil/media-sosial/create')}
|
||||||
|
fz={{ base: 'xs', sm: 'sm' }}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
|
||||||
<Table highlightOnHover>
|
<Box>
|
||||||
<TableThead>
|
{/* Desktop: Table | Mobile: Card-based vertical layout */}
|
||||||
<TableTr>
|
<Box visibleFrom="md">
|
||||||
<TableTh style={{ width: '25%' }}>Nama Media Sosial / Kontak</TableTh>
|
<Table highlightOnHover>
|
||||||
<TableTh style={{ width: '20%' }}>Gambar</TableTh>
|
<TableThead>
|
||||||
<TableTh style={{ width: '20%' }}>Link / No. Telepon</TableTh>
|
<TableTr>
|
||||||
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
|
<TableTh style={{ width: '25%' }}>
|
||||||
</TableTr>
|
<Text fw={600} fz="md" lh={1.45}>
|
||||||
</TableThead>
|
Nama Media Sosial / Kontak
|
||||||
<TableTbody>
|
</Text>
|
||||||
{filteredData.length > 0 ? (
|
</TableTh>
|
||||||
filteredData.map((item) => (
|
<TableTh style={{ width: '20%' }}>
|
||||||
<TableTr key={item.id}>
|
<Text fw={600} fz="md" lh={1.45}>
|
||||||
<TableTd style={{ width: '25%', }}>
|
Gambar
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTh>
|
||||||
<TableTd style={{ width: '20%', }}>
|
<TableTh style={{ width: '20%' }}>
|
||||||
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden', }}>
|
<Text fw={600} fz="md" lh={1.45}>
|
||||||
{item.image?.link ? (
|
Link / No. Telepon
|
||||||
<Image loading='lazy' src={item.image.link} alt={item.name} fit="cover" />
|
</Text>
|
||||||
) : (
|
</TableTh>
|
||||||
<Box bg={colors['blue-button']} w="100%" h="100%" />
|
<TableTh style={{ width: '15%' }}>
|
||||||
)}
|
<Text fw={600} fz="md" lh={1.45}>
|
||||||
</Box>
|
Aksi
|
||||||
</TableTd>
|
</Text>
|
||||||
<TableTd style={{ width: '20%', }}>
|
</TableTh>
|
||||||
<Box w={250}>
|
</TableTr>
|
||||||
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
|
</TableThead>
|
||||||
{item.iconUrl || item.noTelp || '-'}
|
<TableTbody>
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<TableTr key={item.id}>
|
||||||
|
<TableTd style={{ width: '25%' }}>
|
||||||
|
<Text fw={500} fz="md" lh={1.5} truncate="end" lineClamp={1}>
|
||||||
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</TableTd>
|
||||||
</TableTd>
|
<TableTd style={{ width: '20%' }}>
|
||||||
<TableTd style={{ width: '15%' }}>
|
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}>
|
||||||
<Button
|
{(() => {
|
||||||
size="xs"
|
const src = getIconSource(item);
|
||||||
radius="md"
|
if (src) {
|
||||||
variant="light"
|
return (
|
||||||
color="blue"
|
<Image
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
loading="lazy"
|
||||||
onClick={() => router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)}
|
src={src}
|
||||||
>
|
alt={item.name}
|
||||||
Detail
|
fit={item.image?.link ? 'cover' : 'contain'}
|
||||||
</Button>
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
||||||
|
})()}
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '20%' }}>
|
||||||
|
<Box w={250}>
|
||||||
|
<Text truncate fz="sm" lh={1.5} c="dimmed" lineClamp={1}>
|
||||||
|
{item.iconUrl || item.noTelp || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ width: '15%' }}>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py={20}>
|
||||||
|
<Text c="dimmed" fz="md" lh={1.5}>
|
||||||
|
Tidak ada data media sosial yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
)}
|
||||||
) : (
|
</TableTbody>
|
||||||
<TableTr>
|
</Table>
|
||||||
<TableTd colSpan={4}>
|
</Box>
|
||||||
<Center py={20}>
|
|
||||||
<Text color="dimmed">Tidak ada data media sosial yang cocok</Text>
|
{/* Mobile layout */}
|
||||||
</Center>
|
<Stack hiddenFrom="md" gap="xs">
|
||||||
</TableTd>
|
{filteredData.length > 0 ? (
|
||||||
</TableTr>
|
filteredData.map((item) => (
|
||||||
)}
|
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||||
</TableTbody>
|
<Box>
|
||||||
</Table>
|
<Text fz="sm" fw={600} lh={1.4}>Nama Media Sosial / Kontak</Text>
|
||||||
|
<Text fw={500} fz="sm" lh={1.45}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Gambar</Text>
|
||||||
|
</Box>
|
||||||
|
<Box w={40} h={40} style={{ borderRadius: 6, overflow: 'hidden' }}>
|
||||||
|
{(() => {
|
||||||
|
const src = getIconSource(item);
|
||||||
|
if (src) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
loading="lazy"
|
||||||
|
src={src}
|
||||||
|
alt={item.name}
|
||||||
|
fit={item.image?.link ? 'cover' : 'contain'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
||||||
|
})()}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Link / No. Telepon</Text>
|
||||||
|
<a
|
||||||
|
href={item.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
c="blue"
|
||||||
|
truncate
|
||||||
|
>
|
||||||
|
{item.iconUrl || item.noTelp || '-'}
|
||||||
|
</Text>
|
||||||
|
</a>
|
||||||
|
</Box>
|
||||||
|
<Group mt="sm" justify="flex-end">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/landing-page/profil/media-sosial/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Center py={24}>
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
Tidak ada data media sosial yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
@@ -138,4 +283,4 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MediaSosial;
|
export default MediaSosial;
|
||||||
@@ -178,7 +178,7 @@ function EditPejabatDesa() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page
|
|||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
@@ -35,15 +34,15 @@ function Page() {
|
|||||||
<Title order={3} c={colors['blue-button']}>Preview Pejabat Desa</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Pejabat Desa</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
style={{fontSize: 15, fontWeight: "bold"}}
|
||||||
variant="light"
|
c="green"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push(`/admin/landing-page/profil/pejabat-desa/${allList.findUnique.data?.id}`)}
|
onClick={() => router.push(`/admin/landing-page/profil/pejabat-desa/${allList.findUnique.data?.id}`)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
{dataArray.map((item) => (
|
{dataArray.map((item) => (
|
||||||
@@ -52,7 +51,7 @@ function Page() {
|
|||||||
<Grid>
|
<Grid>
|
||||||
<GridCol span={12}>
|
<GridCol span={12}>
|
||||||
<Center>
|
<Center>
|
||||||
<Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" loading="lazy"/>
|
<Image src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" loading="lazy" />
|
||||||
</Center>
|
</Center>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={12}>
|
<GridCol span={12}>
|
||||||
@@ -93,7 +92,7 @@ function Page() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
<Box mt="lg">
|
<Box mt="lg">
|
||||||
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Jabatan</Text>
|
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Jabatan</Text>
|
||||||
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']}>
|
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="left" c={colors['blue-button']}>
|
||||||
{item.position}
|
{item.position}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ function EditProgramInovasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -40,13 +40,15 @@ function DetailProgramInovasi() {
|
|||||||
const data = stateProgramInovasi.findUnique.data
|
const data = stateProgramInovasi.findUnique.data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'md', md: 'xl' }} py="lg">
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button variant="subtle" onClick={() => router.back()} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}>
|
<Box pb="20">
|
||||||
Kembali
|
<Button variant="subtle" onClick={() => router.back()} leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}>
|
||||||
</Button>
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: "100%", md: "60%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -68,9 +70,9 @@ function DetailProgramInovasi() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold">Gambar</Text>
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
{data.image?.link ? (
|
{data.image?.link ? (
|
||||||
<Image
|
<Image
|
||||||
src={data.image.link}
|
src={data.image.link}
|
||||||
alt="Gambar Program"
|
alt="Gambar Program"
|
||||||
radius="md"
|
radius="md"
|
||||||
style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain' }}
|
style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain' }}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@@ -106,28 +108,28 @@ function DetailProgramInovasi() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedId(data.id);
|
setSelectedId(data.id);
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}}
|
}}
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() => router.push(`/admin/landing-page/profil/program-inovasi/${data.id}/edit`)}
|
onClick={() => router.push(`/admin/landing-page/profil/program-inovasi/${data.id}/edit`)}
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function CreateProgramInovasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } 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 { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconPlus, 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';
|
||||||
@@ -13,7 +13,7 @@ function ProgramInovasi() {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px="md" py="lg">
|
<Box px={{base: 0, md: "md"}} py="lg">
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title="Program Inovasi"
|
title="Program Inovasi"
|
||||||
placeholder="Cari program inovasi..."
|
placeholder="Cari program inovasi..."
|
||||||
@@ -29,12 +29,13 @@ function ProgramInovasi() {
|
|||||||
function ListProgramInovasi({ search }: { search: string }) {
|
function ListProgramInovasi({ search }: { search: string }) {
|
||||||
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
|
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
|
||||||
|
|
||||||
const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
|
const { data, page, totalPages, loading, load } = stateProgramInovasi.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search);
|
load(page, 10, debouncedSearch);
|
||||||
}, [page, search]);
|
}, [page, debouncedSearch]);
|
||||||
|
|
||||||
const filteredData = data || [];
|
const filteredData = data || [];
|
||||||
|
|
||||||
@@ -52,75 +53,144 @@ function ListProgramInovasi({ search }: { search: string }) {
|
|||||||
<Group justify='space-between'>
|
<Group justify='space-between'>
|
||||||
<Title order={4}>Daftar Program Inovasi</Title>
|
<Title order={4}>Daftar Program Inovasi</Title>
|
||||||
<Button
|
<Button
|
||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="md"
|
radius="md"
|
||||||
onClick={() => router.push('/admin/landing-page/profil/program-inovasi/create')}
|
onClick={() => router.push('/admin/landing-page/profil/program-inovasi/create')}
|
||||||
>
|
>
|
||||||
Tambah Program
|
Tambah Program
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box visibleFrom='md'>
|
||||||
<Table highlightOnHover striped verticalSpacing="sm">
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<TableThead>
|
<Table highlightOnHover striped verticalSpacing="sm">
|
||||||
<TableTr>
|
<TableThead>
|
||||||
<TableTh>Nama Program</TableTh>
|
|
||||||
<TableTh>Deskripsi</TableTh>
|
|
||||||
<TableTh>Link</TableTh>
|
|
||||||
<TableTh>Aksi</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{filteredData.length === 0 ? (
|
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={4}>
|
<TableTh>Nama Program</TableTh>
|
||||||
<Center py={20}>
|
<TableTh>Deskripsi</TableTh>
|
||||||
<Text color="dimmed">Belum ada data program inovasi</Text>
|
<TableTh>Link</TableTh>
|
||||||
</Center>
|
<TableTh>Aksi</TableTh>
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
</TableTr>
|
||||||
) : (
|
</TableThead>
|
||||||
filteredData.map((item) => (
|
<TableTbody>
|
||||||
<TableTr key={item.id}>
|
{filteredData.length === 0 ? (
|
||||||
<TableTd>
|
<TableTr>
|
||||||
<Text fw={500}>{item.name}</Text>
|
<TableTd colSpan={4}>
|
||||||
</TableTd>
|
<Center py={20}>
|
||||||
<TableTd style={{ maxWidth: 250 }}>
|
<Text color="dimmed">Belum ada data program inovasi</Text>
|
||||||
<Text fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }}></Text>
|
</Center>
|
||||||
</TableTd>
|
|
||||||
<TableTd style={{ maxWidth: 250 }}>
|
|
||||||
<Tooltip label="Buka tautan program" position="top" withArrow>
|
|
||||||
<a
|
|
||||||
href={item.link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
style={{ color: colors['blue-button'], textDecoration: 'underline' }}
|
|
||||||
>
|
|
||||||
<Text truncate fz="sm">{item.link}</Text>
|
|
||||||
</a>
|
|
||||||
</Tooltip>
|
|
||||||
</TableTd>
|
|
||||||
<TableTd>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
radius="md"
|
|
||||||
variant="light"
|
|
||||||
color="blue"
|
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
|
||||||
onClick={() =>
|
|
||||||
router.push(`/admin/landing-page/profil/program-inovasi/${item.id}`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Detail
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
) : (
|
||||||
)}
|
filteredData.map((item) => (
|
||||||
</TableTbody>
|
<TableTr key={item.id}>
|
||||||
</Table>
|
<TableTd>
|
||||||
|
<Text fw={500}>{item.name}</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ maxWidth: 250 }}>
|
||||||
|
<Text fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }}></Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd style={{ maxWidth: 250 }}>
|
||||||
|
<Tooltip label="Buka tautan program" position="top" withArrow>
|
||||||
|
<a
|
||||||
|
href={item.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: colors['blue-button'], textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
<Text truncate fz="sm">{item.link}</Text>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/landing-page/profil/program-inovasi/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box hiddenFrom="md" pt={20}>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{filteredData.map((item) => (
|
||||||
|
<Paper
|
||||||
|
key={item.id}
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p="md"
|
||||||
|
shadow="xs"
|
||||||
|
>
|
||||||
|
<Stack gap={6}>
|
||||||
|
{/* Title */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Nama Program</Text>
|
||||||
|
<Text fw={500} lh={1.4}>{item.name}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
||||||
|
<Text fz="sm" c="gray.7" lineClamp={2}>
|
||||||
|
{item.description || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Link */}
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>Link</Text>
|
||||||
|
<a
|
||||||
|
href={item.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ textDecoration: 'underline' }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
c="blue"
|
||||||
|
truncate
|
||||||
|
>
|
||||||
|
{item.link}
|
||||||
|
</Text>
|
||||||
|
</a>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action */}
|
||||||
|
<Group justify="flex-end" mt="xs">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/admin/landing-page/profil/program-inovasi/${item.id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{filteredData.length > 0 && (
|
{filteredData.length > 0 && (
|
||||||
<Center mt="md">
|
<Center mt="md">
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function EditDaftarInformasiPublik() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ function DetailDaftarInformasiPublik() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: '100%', md: '60%' }}
|
w={{ base: '100%', md: '70%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -83,39 +83,39 @@ function DetailDaftarInformasiPublik() {
|
|||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold" mb={4}>Deskripsi</Text>
|
<Text fz="lg" fw="bold" mb={4}>Deskripsi</Text>
|
||||||
<Box
|
<Text
|
||||||
fz="md"
|
px={"xs"}
|
||||||
c="dimmed"
|
fz="md"
|
||||||
|
c="dimmed"
|
||||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||||
className="prose max-w-none"
|
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm" mt="md">
|
<Group gap="sm" mt="md">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
leftSection={<IconEdit size={18} />}
|
leftSection={<IconEdit size={18} />}
|
||||||
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${data.id}/edit`)}
|
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${data.id}/edit`)}
|
||||||
disabled={!data}
|
disabled={!data}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
leftSection={<IconTrash size={18} />}
|
leftSection={<IconTrash size={18} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedId(data.id);
|
setSelectedId(data.id);
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}}
|
}}
|
||||||
disabled={stateDaftarInformasi.delete.loading || !data}
|
disabled={stateDaftarInformasi.delete.loading || !data}
|
||||||
loading={stateDaftarInformasi.delete.loading}
|
loading={stateDaftarInformasi.delete.loading}
|
||||||
>
|
>
|
||||||
Hapus
|
Hapus
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function CreateDaftarInformasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import {
|
||||||
import { useShallowEffect, useViewportSize } from '@mantine/hooks';
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useDebouncedValue, useShallowEffect, useViewportSize } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconPlus, 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';
|
||||||
@@ -10,7 +27,7 @@ import HeaderSearch from '../../_com/header';
|
|||||||
import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
|
import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
|
||||||
|
|
||||||
function DaftarInformasiPublik() {
|
function DaftarInformasiPublik() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
@@ -26,102 +43,158 @@ function DaftarInformasiPublik() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListDaftarInformasi({ search }: { search: string }) {
|
function ListDaftarInformasi({ search }: { search: string }) {
|
||||||
const listData = useProxy(daftarInformasiPublik)
|
const listData = useProxy(daftarInformasiPublik);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
|
|
||||||
const { data, page, totalPages, loading, load } = listData.findMany
|
const { data, page, totalPages, loading, load } = listData.findMany;
|
||||||
const { width } = useViewportSize()
|
const { width } = useViewportSize();
|
||||||
const isMobile = width < 768
|
const isMobile = width < 768;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search)
|
load(page, 10, debouncedSearch);
|
||||||
}, [page, search])
|
}, [page, debouncedSearch]);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || [];
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py="md">
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py="md">
|
<Box py={{ base: 'md', md: 'lg' }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||||
<Title order={4}>List Daftar Informasi Publik</Title>
|
<Title order={2} lh={1.2}>
|
||||||
<Button
|
List Daftar Informasi Publik
|
||||||
leftSection={<IconPlus size={18} />}
|
</Title>
|
||||||
color="blue"
|
<Button
|
||||||
variant="light"
|
leftSection={<IconPlus size={18} />}
|
||||||
onClick={() => router.push('/admin/ppid/daftar-informasi-publik/create')}
|
color="blue"
|
||||||
>
|
variant="light"
|
||||||
{isMobile ? 'Tambah' : 'Tambah Baru'}
|
onClick={() => router.push('/admin/ppid/daftar-informasi-publik/create')}
|
||||||
</Button>
|
>
|
||||||
|
{isMobile ? 'Tambah' : 'Tambah Baru'}
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{filteredData.length === 0 ? (
|
{filteredData.length === 0 ? (
|
||||||
<Stack align="center" py="xl">
|
<Stack align="center" py="xl">
|
||||||
<IconDeviceImacCog size={40} stroke={1.5} color={colors['blue-button']} />
|
<IconDeviceImacCog size={40} stroke={1.5} color={colors['blue-button']} />
|
||||||
<Text fw={500} c="dimmed">Belum ada informasi publik yang tersedia</Text>
|
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
|
||||||
|
Belum ada informasi publik yang tersedia
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<>
|
||||||
<Table
|
{/* Desktop Table */}
|
||||||
highlightOnHover
|
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||||
striped
|
<Table
|
||||||
stickyHeader
|
highlightOnHover
|
||||||
style={{ minWidth: '700px' }}
|
striped
|
||||||
>
|
stickyHeader
|
||||||
<TableThead>
|
style={{ minWidth: '700px' }}
|
||||||
<TableTr>
|
>
|
||||||
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
|
<TableThead>
|
||||||
<TableTh style={{ width: '25%' }}>Jenis Informasi</TableTh>
|
<TableTr>
|
||||||
<TableTh style={{ width: '40%' }}>Deskripsi</TableTh>
|
<TableTh w="25%">
|
||||||
<TableTh style={{ width: '20%', textAlign: 'center' }}>Aksi</TableTh>
|
<Text fw={600} lh={1.4}>
|
||||||
</TableTr>
|
Jenis Informasi
|
||||||
</TableThead>
|
</Text>
|
||||||
<TableTbody>
|
</TableTh>
|
||||||
{filteredData.map((item, index) => (
|
<TableTh w="40%">
|
||||||
<TableTr key={item.id}>
|
<Text fw={600} lh={1.4}>
|
||||||
<TableTd style={{ textAlign: 'center' }}>
|
Deskripsi
|
||||||
<Text fz="sm">{(page - 1) * 10 + index + 1}</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTh>
|
||||||
<TableTd>
|
<TableTh ta="center" w="20%">
|
||||||
<Box w={200}>
|
<Text fw={600} lh={1.4}>
|
||||||
<Text fw={500} lineClamp={1}>{item.jenisInformasi}</Text>
|
Aksi
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTh>
|
||||||
<TableTd>
|
</TableTr>
|
||||||
<Box w={200}>
|
</TableThead>
|
||||||
<Text lineClamp={1} fz="sm" c="dimmed">
|
<TableTbody>
|
||||||
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80) + '...'}
|
{filteredData.map((item) => (
|
||||||
</Text>
|
<TableTr key={item.id}>
|
||||||
</Box>
|
<TableTd>
|
||||||
|
<Text fz="sm" fw={600} lh={1.5} lineClamp={1}>
|
||||||
</TableTd>
|
{item.jenisInformasi}
|
||||||
<TableTd style={{ textAlign: 'center' }}>
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1}>
|
||||||
|
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd ta="center">
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
onClick={() => router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)}
|
onClick={() =>
|
||||||
|
router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
))}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Card List */}
|
||||||
|
<Stack hiddenFrom="md" gap="sm">
|
||||||
|
{filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Box>
|
||||||
|
<Text fw={600} lh={1.4}>
|
||||||
|
Jenis Informasi
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4}>
|
||||||
|
{item.jenisInformasi}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fw={600} lh={1.4}>
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.5} c="dimmed">
|
||||||
|
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/ppid/daftar-informasi-publik/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
<Center mt="lg">
|
|
||||||
|
<Center mt={{ base: 'lg', md: 'xl' }}>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
@@ -129,14 +202,12 @@ function ListDaftarInformasi({ search }: { search: string }) {
|
|||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
mt="md"
|
|
||||||
mb="md"
|
|
||||||
color="blue"
|
color="blue"
|
||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DaftarInformasiPublik;
|
export default DaftarInformasiPublik;
|
||||||
@@ -8,11 +8,11 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum';
|
import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const listDasarHukum = useProxy(stateDasarHukumPPID)
|
const listDasarHukum = useProxy(stateDasarHukumPPID);
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
listDasarHukum.findById.load('1')
|
listDasarHukum.findById.load('1');
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (listDasarHukum.findById.loading) {
|
if (listDasarHukum.findById.loading) {
|
||||||
return (
|
return (
|
||||||
@@ -40,15 +40,16 @@ function Page() {
|
|||||||
<Title order={3} c={colors['blue-button']}>Preview Dasar Hukum PPID</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Dasar Hukum PPID</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
w={{ base: '100%', md: "110%" }}
|
||||||
variant="light"
|
c="green"
|
||||||
leftSection={<IconEdit size={18} stroke={2} />}
|
variant="light"
|
||||||
radius="md"
|
leftSection={<IconEdit size={18} stroke={2} />}
|
||||||
onClick={() => router.push('/admin/ppid/dasar-hukum/edit')}
|
radius="md"
|
||||||
>
|
onClick={() => router.push('/admin/ppid/dasar-hukum/edit')}
|
||||||
Edit
|
>
|
||||||
</Button>
|
Edit
|
||||||
|
</Button>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -57,33 +58,39 @@ function Page() {
|
|||||||
<Grid>
|
<Grid>
|
||||||
<GridCol span={12}>
|
<GridCol span={12}>
|
||||||
<Center>
|
<Center>
|
||||||
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
|
<Image loading="lazy" src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo PPID" />
|
||||||
</Center>
|
</Center>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={12}>
|
<GridCol span={12}>
|
||||||
<Text
|
<Title
|
||||||
ta="center"
|
order={3}
|
||||||
fz={{ base: '1.5rem', md: '2rem' }}
|
ta="center"
|
||||||
fw="bold"
|
lh={{ base: 1.15, md: 1.1 }}
|
||||||
|
fw="bold"
|
||||||
c={colors['blue-button']}
|
c={colors['blue-button']}
|
||||||
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
|
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||||
/>
|
/>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Divider my="xl" color={colors['blue-button']} />
|
<Divider my="xl" color={colors['blue-button']} />
|
||||||
|
|
||||||
<Box
|
<Text
|
||||||
className="prose max-w-none"
|
|
||||||
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}
|
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal", fontSize: '1.1rem', lineHeight: 1.7, textAlign: 'justify' }}
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
fontSize: '1rem',
|
||||||
|
lineHeight: 1.55,
|
||||||
|
textAlign: 'justify',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
@@ -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 { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title} 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';
|
||||||
@@ -56,42 +56,77 @@ function LayoutTabsIKM({ children }: { children: React.ReactNode }) {
|
|||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
<TabsList
|
<Box visibleFrom='md' pb={10}>
|
||||||
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)",
|
||||||
display: "flex",
|
borderRadius: "1rem",
|
||||||
flexWrap: "nowrap",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
gap: "0.5rem",
|
display: "flex",
|
||||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
flexWrap: "nowrap",
|
||||||
}}
|
gap: "0.5rem",
|
||||||
>
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
{tabs.map((e, i) => (
|
}}
|
||||||
<Tooltip
|
>
|
||||||
key={i}
|
{tabs.map((tab, i) => (
|
||||||
label={e.description}
|
|
||||||
withArrow
|
|
||||||
position="bottom"
|
|
||||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
|
||||||
>
|
|
||||||
<TabsTab
|
<TabsTab
|
||||||
value={e.value}
|
key={i}
|
||||||
leftSection={e.icon}
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 500,
|
fontWeight: 600,
|
||||||
fontSize: "0.9rem",
|
fontSize: "0.9rem",
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{e.label}
|
{tab.label}
|
||||||
</TabsTab>
|
</TabsTab>
|
||||||
</Tooltip>
|
))}
|
||||||
))}
|
</TabsList>
|
||||||
</TabsList>
|
</ScrollArea>
|
||||||
</ScrollArea>
|
</Box>
|
||||||
|
|
||||||
|
<Box hiddenFrom='md' pb={10}>
|
||||||
|
<ScrollArea
|
||||||
|
type="auto"
|
||||||
|
offsetScrollbars={false}
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
|
||||||
|
<TabsList
|
||||||
|
p="xs" // lebih kecil
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
width: "max-content", // ⬅️ kunci
|
||||||
|
maxWidth: "100%", // ⬅️ penting
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsTab
|
||||||
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
paddingInline: "0.75rem", // ⬅️ lebih ramping
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</TabsTab>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
{tabs.map((e, i) => (
|
{tabs.map((e, i) => (
|
||||||
<TabsPanel key={i} value={e.value} mt="md">
|
<TabsPanel key={i} value={e.value} mt="md">
|
||||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||||
|
|||||||
@@ -85,11 +85,11 @@ function EditResponden() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Responden
|
Edit Responden
|
||||||
</Title>
|
</Title>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function DetailResponden() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ function RespondenCreate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Box mb={10}>
|
<Box mb={10}>
|
||||||
<Button variant="subtle" onClick={() => router.back()}>
|
<Button variant="subtle" onClick={() => router.back()}>
|
||||||
<IconArrowBack size={20} />
|
<IconArrowBack size={20} />
|
||||||
@@ -96,24 +96,24 @@ function RespondenCreate() {
|
|||||||
}
|
}
|
||||||
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
|
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
key={"rating_responden"}
|
key={"rating_responden"}
|
||||||
label={"Rating"}
|
label={"Rating"}
|
||||||
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
|
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
|
||||||
value={stategrafikBerdasarkanResponden.create.form.ratingId || ""}
|
value={stategrafikBerdasarkanResponden.create.form.ratingId || ""}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
stategrafikBerdasarkanResponden.create.form.ratingId = val ?? "";
|
stategrafikBerdasarkanResponden.create.form.ratingId = val ?? "";
|
||||||
}}
|
}}
|
||||||
data={
|
data={
|
||||||
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
|
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
|
||||||
.filter(Boolean) // Hapus null, undefined, dll
|
.filter(Boolean) // Hapus null, undefined, dll
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
label: item.name || 'Tanpa Nama',
|
label: item.name || 'Tanpa Nama',
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
|
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
key={"kelompokUmur"}
|
key={"kelompokUmur"}
|
||||||
label={"Kelompok Umur"}
|
label={"Kelompok Umur"}
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -9,11 +25,11 @@ import HeaderSearch from '../../../_com/header';
|
|||||||
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
|
import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan';
|
||||||
|
|
||||||
function Responden() {
|
function Responden() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState('');
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
title="Data Responden"
|
title="Responden"
|
||||||
placeholder="Cari nama responden..."
|
placeholder="Cari nama responden..."
|
||||||
searchIcon={<IconSearch size={18} />}
|
searchIcon={<IconSearch size={18} />}
|
||||||
value={search}
|
value={search}
|
||||||
@@ -33,17 +49,13 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data, page, totalPages, loading, load } = state.findMany;
|
const { data, page, totalPages, loading, load } = state.findMany;
|
||||||
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10)
|
load(page, 10);
|
||||||
}, [page]);
|
}, [page]);
|
||||||
|
|
||||||
|
const filteredData = (data || []).filter((item) => {
|
||||||
const filteredData = (data || []).filter(item => {
|
|
||||||
const keyword = search.toLowerCase();
|
const keyword = search.toLowerCase();
|
||||||
return (
|
return item.name.toLowerCase().includes(keyword);
|
||||||
item.name.toLowerCase().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
@@ -56,21 +68,25 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
|
<Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={3}>Data Responden</Title>
|
<Title order={2} lh={1.2}>
|
||||||
<Table striped withRowBorders>
|
Data Responden
|
||||||
<TableThead>
|
</Title>
|
||||||
<TableTr>
|
<Box visibleFrom="md">
|
||||||
<TableTh style={{ textAlign: 'center' }}>No</TableTh>
|
<Table striped withRowBorders>
|
||||||
<TableTh>Nama</TableTh>
|
<TableThead>
|
||||||
<TableTh>Tanggal</TableTh>
|
<TableTr>
|
||||||
<TableTh>Jenis Kelamin</TableTh>
|
<TableTh ta="center">No</TableTh>
|
||||||
<TableTh style={{ textAlign: 'center' }}>Aksi</TableTh>
|
<TableTh>Nama</TableTh>
|
||||||
</TableTr>
|
<TableTh>Tanggal</TableTh>
|
||||||
</TableThead>
|
<TableTh>Jenis Kelamin</TableTh>
|
||||||
</Table>
|
<TableTh ta="center">Aksi</TableTh>
|
||||||
<Text c="dimmed" ta="center" py="md">
|
</TableTr>
|
||||||
|
</TableThead>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
<Text c="dimmed" ta="center" py="md" fz={{ base: 'sm', md: 'md' }} lh={1.4}>
|
||||||
Belum ada data responden yang tersedia
|
Belum ada data responden yang tersedia
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -79,54 +95,133 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper withBorder bg="white" p="lg" radius="md" shadow="sm">
|
<Paper withBorder bg="white" p={{ base: 'md', sm: 'lg' }} radius="md" shadow="sm">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Title order={3}>Data Responden</Title>
|
<Title order={2} lh={1.2}>
|
||||||
<Table striped withRowBorders>
|
Data Responden
|
||||||
<TableThead>
|
</Title>
|
||||||
<TableTr>
|
|
||||||
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
|
{/* Desktop Table */}
|
||||||
<TableTh style={{ width: '25%' }}>Nama</TableTh>
|
<Box visibleFrom="md">
|
||||||
<TableTh style={{ width: '25%' }}>Tanggal</TableTh>
|
<Table striped withRowBorders>
|
||||||
<TableTh style={{ width: '20%' }}>Jenis Kelamin</TableTh>
|
<TableThead>
|
||||||
<TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
|
|
||||||
</TableTr>
|
|
||||||
</TableThead>
|
|
||||||
<TableTbody>
|
|
||||||
{filteredData.length === 0 ? (
|
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={5}>
|
<TableTh w="5%" ta="center">
|
||||||
<Text c="dimmed" ta="center" py="md">
|
No
|
||||||
Tidak ada data yang cocok dengan pencarian
|
</TableTh>
|
||||||
</Text>
|
<TableTh w="25%">Nama</TableTh>
|
||||||
</TableTd>
|
<TableTh w="25%">Tanggal</TableTh>
|
||||||
|
<TableTh w="20%">Jenis Kelamin</TableTh>
|
||||||
|
<TableTh w="15%" ta="center">
|
||||||
|
Aksi
|
||||||
|
</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
) : (
|
</TableThead>
|
||||||
filteredData.map((item, index) => (
|
<TableTbody>
|
||||||
<TableTr key={item.id}>
|
{filteredData.length === 0 ? (
|
||||||
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
|
<TableTr>
|
||||||
<TableTd style={{ width: '25%' }}>{item.name}</TableTd>
|
<TableTd colSpan={5}>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}>
|
||||||
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID') : '-'}
|
Tidak ada data yang cocok dengan pencarian
|
||||||
</TableTd>
|
</Text>
|
||||||
<TableTd style={{ width: '20%' }}>{item.jenisKelamin.name}</TableTd>
|
|
||||||
<TableTd style={{ width: '15%', textAlign: 'center' }}>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
radius="md"
|
|
||||||
variant="light"
|
|
||||||
color="blue"
|
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
|
||||||
onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)}
|
|
||||||
>
|
|
||||||
Detail
|
|
||||||
</Button>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
) : (
|
||||||
)}
|
filteredData.map((item, index) => (
|
||||||
</TableTbody>
|
<TableTr key={item.id}>
|
||||||
</Table>
|
<TableTd ta="center">{index + 1}</TableTd>
|
||||||
|
<TableTd>{item.name}</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID') : '-'}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>{item.jenisKelamin.name}</TableTd>
|
||||||
|
<TableTd ta="center">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Card View */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
{filteredData.length === 0 ? (
|
||||||
|
<Text c="dimmed" ta="center" py="md" fz="sm" lh={1.4}>
|
||||||
|
Tidak ada data yang cocok dengan pencarian
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Stack gap="sm">
|
||||||
|
{filteredData.map((item, index) => (
|
||||||
|
<Paper key={item.id} withBorder p="sm" radius="md">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>
|
||||||
|
No
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4}>
|
||||||
|
{index + 1}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>
|
||||||
|
Nama
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>
|
||||||
|
Tanggal
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4}>
|
||||||
|
{item.tanggal
|
||||||
|
? new Date(item.tanggal).toLocaleDateString('id-ID')
|
||||||
|
: '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>
|
||||||
|
Jenis Kelamin
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4}>
|
||||||
|
{item.jenisKelamin.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box ta="center">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/ppid/ikm-desa-darmasaba/responden/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
{filteredData.length > 0 && (
|
{filteredData.length > 0 && (
|
||||||
<Center>
|
<Center>
|
||||||
<Pagination
|
<Pagination
|
||||||
@@ -138,7 +233,6 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
}}
|
}}
|
||||||
size="md"
|
size="md"
|
||||||
radius="md"
|
radius="md"
|
||||||
mt="md"
|
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
@@ -147,6 +241,4 @@ function ListResponden({ search }: ListRespondenProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Responden;
|
export default Responden;
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ function DetailPermohonanInformasiPublik() {
|
|||||||
const data = state.findUnique.data;
|
const data = state.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py="md">
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -39,7 +39,7 @@ function DetailPermohonanInformasiPublik() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: '100%', md: '60%' }}
|
w={{ base: '100%', md: '70%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -1,105 +1,277 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors'
|
import colors from '@/con/colors'
|
||||||
import { Box, Button, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'
|
import {
|
||||||
import { useShallowEffect } from '@mantine/hooks'
|
Box,
|
||||||
import { IconDeviceImacCog, IconId, IconInfoCircle, IconPhone, IconUser } from '@tabler/icons-react'
|
Button,
|
||||||
|
Center,
|
||||||
|
Grid,
|
||||||
|
GridCol,
|
||||||
|
Group,
|
||||||
|
Pagination,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core'
|
||||||
|
import {
|
||||||
|
IconDeviceImacCog,
|
||||||
|
IconId,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconPhone,
|
||||||
|
IconSearch,
|
||||||
|
IconUser,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { useProxy } from 'valtio/utils'
|
import { useProxy } from 'valtio/utils'
|
||||||
import statepermohonanInformasiPublikForm from '../../_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik'
|
import statepermohonanInformasiPublikForm from '../../_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik'
|
||||||
|
import { useDebouncedValue } from '@mantine/hooks'
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const permohonanInformasiPublikState = useProxy(statepermohonanInformasiPublikForm)
|
const permohonanInformasiPublikState = useProxy(statepermohonanInformasiPublikForm)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { data, page, totalPages, loading, load } = permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useEffect(() => {
|
||||||
permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.load()
|
load(page, 10, debouncedSearch);
|
||||||
}, [])
|
}, [page, debouncedSearch]);
|
||||||
|
|
||||||
if (!permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Stack pos="relative" bg={colors.Bg} p="lg" align="center">
|
<Stack pos="relative" p="lg" align="center">
|
||||||
<Skeleton radius="md" h={40} w="60%" />
|
|
||||||
<Skeleton radius="md" h={200} w="100%" />
|
<Skeleton radius="md" h={200} w="100%" />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box py={{ base: 'md', md: 'lg' }}>
|
||||||
|
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
|
||||||
|
<Stack gap={'sm'}>
|
||||||
|
<Grid mb={10}>
|
||||||
|
<GridCol span={{ base: 12, md: 9 }}>
|
||||||
|
<Title order={2} lh={1.2} c="dark">
|
||||||
|
Daftar Permohonan Informasi Publik
|
||||||
|
</Title>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, md: 3 }}>
|
||||||
|
<Paper radius="lg" bg={colors['white-1']}>
|
||||||
|
<TextInput
|
||||||
|
radius="lg"
|
||||||
|
placeholder={"Cari nama..."}
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
w="100%"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
|
<Stack align="center" py="xl" ta="center">
|
||||||
|
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
|
||||||
|
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
|
||||||
|
{search
|
||||||
|
? 'Tidak ditemukan data yang sesuai dengan pencarian'
|
||||||
|
: 'Belum ada permohonan yang tercatat'
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const data = permohonanInformasiPublikState.statepermohonanInformasiPublik.findMany.data
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py="md">
|
<Box py={{ base: 'sm', md: 'md' }}>
|
||||||
<Paper bg={colors['white-1']} p="lg" radius="xl" shadow="sm" withBorder>
|
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="xl" shadow="sm" withBorder>
|
||||||
<Stack gap="md">
|
<Stack gap={'sm'}>
|
||||||
<Group justify="space-between">
|
<Grid mb={10}>
|
||||||
<Title order={2} c="dark">Daftar Permohonan Informasi Publik</Title>
|
<GridCol span={{ base: 12, md: 9 }}>
|
||||||
<IconInfoCircle size={20} stroke={1.5} />
|
<Title order={2} lh={1.2} c="dark">
|
||||||
</Group>
|
Daftar Permohonan Informasi Publik
|
||||||
|
</Title>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol span={{ base: 12, md: 3 }}>
|
||||||
|
<Paper radius="lg" bg={colors['white-1']}>
|
||||||
|
<TextInput
|
||||||
|
radius="lg"
|
||||||
|
placeholder={"Cari nama..."}
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
w="100%"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</GridCol>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{data.length === 0 ? (
|
{data.length === 0 ? (
|
||||||
<Stack align="center" py="xl">
|
<Stack align="center" py={{ base: 'xl', md: 'xl' }}>
|
||||||
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
|
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
|
||||||
<Text fw={500} c="dimmed">Belum ada permohonan informasi yang tercatat</Text>
|
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
|
||||||
|
Belum ada permohonan informasi yang tercatat
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<>
|
||||||
<Table
|
{/* Desktop Table */}
|
||||||
highlightOnHover
|
<Box visibleFrom="md">
|
||||||
withRowBorders
|
<Table highlightOnHover>
|
||||||
withColumnBorders
|
<TableThead>
|
||||||
withTableBorder
|
<TableTr>
|
||||||
striped
|
<TableTh fz="sm" fw={600} ta="center" w={60}>
|
||||||
stickyHeader
|
No
|
||||||
>
|
</TableTh>
|
||||||
<TableThead>
|
<TableTh fz="sm" fw={600}>
|
||||||
<TableTr>
|
<Group gap={5}>
|
||||||
<TableTh>No</TableTh>
|
<IconUser size={16} />
|
||||||
<TableTh><Group gap={5}><IconUser size={16} /> Nama</Group></TableTh>
|
Nama
|
||||||
<TableTh><Group gap={5}><IconId size={16} /> NIK</Group></TableTh>
|
</Group>
|
||||||
<TableTh><Group gap={5}><IconPhone size={16} /> Telepon</Group></TableTh>
|
</TableTh>
|
||||||
<TableTh><Group gap={5}><IconInfoCircle size={16} /> Detail</Group></TableTh>
|
<TableTh fz="sm" fw={600}>
|
||||||
</TableTr>
|
<Group gap={5}>
|
||||||
</TableThead>
|
<IconId size={16} />
|
||||||
<TableTbody>
|
NIK
|
||||||
{data.map((item, index) => (
|
</Group>
|
||||||
<TableTr key={item.id}>
|
</TableTh>
|
||||||
<TableTd>{index + 1}</TableTd>
|
<TableTh fz="sm" fw={600}>
|
||||||
<TableTd>
|
<Group gap={5}>
|
||||||
<Box w={200}>
|
<IconPhone size={16} />
|
||||||
<Text lineClamp={1} fw={500}>{item.name}</Text>
|
Telepon
|
||||||
</Box>
|
</Group>
|
||||||
</TableTd>
|
</TableTh>
|
||||||
<TableTd>
|
<TableTh fz="sm" fw={600} w={140}>
|
||||||
<Box w={200}>
|
<Group gap={5}>
|
||||||
{item.nik}
|
<IconInfoCircle size={16} />
|
||||||
</Box>
|
Detail
|
||||||
</TableTd>
|
</Group>
|
||||||
<TableTd>
|
</TableTh>
|
||||||
<Box w={200}>
|
</TableTr>
|
||||||
{item.notelp}
|
</TableThead>
|
||||||
</Box>
|
<TableTbody>
|
||||||
</TableTd>
|
{data.map((item, index) => (
|
||||||
<TableTd>
|
<TableTr key={item.id}>
|
||||||
|
<TableTd ta="center" fz="sm" lh={1.5}>
|
||||||
|
{index + 1}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Text fz="sm" fw={500} lh={1.5} lineClamp={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Text fz="sm" lh={1.5}>{item.nik}</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Text fz="sm" lh={1.5}>{item.notelp}</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<Stack hiddenFrom="md" gap="xs">
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<Paper key={item.id} p="sm" radius="md" withBorder bg="white">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Box>
|
||||||
|
<Text fz="xs" fw={600} lh={1.4} c="dark">
|
||||||
|
No
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.5} c="dark">
|
||||||
|
{index + 1}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="xs" fw={600} lh={1.4} c="dark">
|
||||||
|
Nama
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.5} c="dark" lineClamp={1}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="xs" fw={600} lh={1.4} c="dark">
|
||||||
|
NIK
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" lh={1.5} c="dark">{item.nik}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="xs" fw={600} lh={1.4} c="dark">
|
||||||
|
Telepon
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" lh={1.5} c="dark">{item.notelp}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz="xs" fw={600} lh={1.4} c="dark">
|
||||||
|
Detail
|
||||||
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
onClick={() => router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)}
|
onClick={() =>
|
||||||
|
router.push(`/admin/ppid/permohonan-informasi-publik/${item.id}`)
|
||||||
|
}
|
||||||
|
mt={2}
|
||||||
>
|
>
|
||||||
Detail
|
Lihat Detail
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</Box>
|
||||||
</TableTr>
|
</Stack>
|
||||||
))}
|
</Paper>
|
||||||
</TableTbody>
|
))}
|
||||||
</Table>
|
</Stack>
|
||||||
</Box>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Center mt="xl">
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => {
|
||||||
|
load(newPage, 10);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}}
|
||||||
|
total={totalPages}
|
||||||
|
withEdges
|
||||||
|
withControls
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page
|
export default Page
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user