Compare commits
5 Commits
nico/23-fe
...
fix/admin/
| Author | SHA1 | Date | |
|---|---|---|---|
| b1d28a8322 | |||
| b86a3a85c3 | |||
| fd63bb0fd4 | |||
| f2c9a922a6 | |||
| 92b24440fe |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,6 +31,9 @@ yarn-error.log*
|
|||||||
# env
|
# env
|
||||||
.env*
|
.env*
|
||||||
|
|
||||||
|
# QC
|
||||||
|
QC
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"elysia": "^1.3.5",
|
"elysia": "^1.3.5",
|
||||||
"embla-carousel": "^8.6.0",
|
"embla-carousel": "^8.6.0",
|
||||||
@@ -112,6 +113,7 @@
|
|||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@types/cli-progress": "^3.11.6",
|
"@types/cli-progress": "^3.11.6",
|
||||||
|
"@types/dompurify": "^3.2.0",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ model PrestasiDesa {
|
|||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ model KategoriPrestasiDesa {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PrestasiDesa PrestasiDesa[]
|
PrestasiDesa PrestasiDesa[]
|
||||||
}
|
}
|
||||||
@@ -263,7 +263,7 @@ model Responden {
|
|||||||
kelompokUmurId String
|
kelompokUmurId String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ model JenisKelaminResponden {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
Responden Responden[]
|
Responden Responden[]
|
||||||
}
|
}
|
||||||
@@ -282,7 +282,7 @@ model PilihanRatingResponden {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
Responden Responden[]
|
Responden Responden[]
|
||||||
}
|
}
|
||||||
@@ -292,7 +292,7 @@ model UmurResponden {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
Responden Responden[]
|
Responden Responden[]
|
||||||
}
|
}
|
||||||
@@ -326,6 +326,7 @@ model PosisiOrganisasiPPID {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||||
children PosisiOrganisasiPPID[] @relation("Parent")
|
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
@@ -345,6 +346,7 @@ model PegawaiPPID {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
|
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
|
||||||
strukturOrganisasi StrukturPPID[] // Relasi balik
|
strukturOrganisasi StrukturPPID[] // Relasi balik
|
||||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
@@ -370,7 +372,7 @@ model VisiMisiPPID {
|
|||||||
misi String @db.Text
|
misi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +383,7 @@ model DasarHukumPPID {
|
|||||||
content String @db.Text
|
content String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +400,7 @@ model ProfilePPID {
|
|||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +412,7 @@ model DaftarInformasiPublik {
|
|||||||
tanggal DateTime @db.Date
|
tanggal DateTime @db.Date
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +433,7 @@ model PermohonanInformasiPublik {
|
|||||||
caraMemperolehSalinanInformasiId String?
|
caraMemperolehSalinanInformasiId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +442,7 @@ model JenisInformasiDiminta {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||||
}
|
}
|
||||||
@@ -450,7 +452,7 @@ model CaraMemperolehInformasi {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||||
}
|
}
|
||||||
@@ -460,7 +462,7 @@ model CaraMemperolehSalinanInformasi {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||||
}
|
}
|
||||||
@@ -474,7 +476,7 @@ model FormulirPermohonanKeberatan {
|
|||||||
alasan String
|
alasan String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +533,7 @@ model SejarahDesa {
|
|||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,7 +543,7 @@ model VisiMisiDesa {
|
|||||||
misi String @db.Text
|
misi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,7 +553,7 @@ model LambangDesa {
|
|||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,7 +564,7 @@ model MaskotDesa {
|
|||||||
images ProfileDesaImage[]
|
images ProfileDesaImage[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,9 @@ function normalizeItem(item: Partial<z.infer<typeof ApbdesItemSchema>>): z.infer
|
|||||||
const anggaran = item.anggaran ?? 0;
|
const anggaran = item.anggaran ?? 0;
|
||||||
const realisasi = item.realisasi ?? 0;
|
const realisasi = item.realisasi ?? 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ✅ Formula yang benar
|
// ✅ Formula yang benar
|
||||||
const selisih = anggaran - realisasi; // positif = sisa anggaran, negatif = over budget
|
const selisih = realisasi - anggaran; // positif = sisa anggaran, negatif = over budget
|
||||||
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; // persentase realisasi terhadap anggaran
|
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; // persentase realisasi terhadap anggaran
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -55,10 +55,15 @@ const programInovasi = proxy({
|
|||||||
programInovasi.findMany.load();
|
programInovasi.findMany.load();
|
||||||
return toast.success("Sukses menambahkan");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log(res);
|
||||||
|
}
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log((error as Error).message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Create error:", error);
|
||||||
|
}
|
||||||
|
toast.error("Gagal menambahkan data");
|
||||||
} finally {
|
} finally {
|
||||||
programInovasi.create.loading = false;
|
programInovasi.create.loading = false;
|
||||||
}
|
}
|
||||||
@@ -91,13 +96,17 @@ const programInovasi = proxy({
|
|||||||
programInovasi.findMany.total = res.data.total || 0;
|
programInovasi.findMany.total = res.data.total || 0;
|
||||||
programInovasi.findMany.totalPages = res.data.totalPages || 1;
|
programInovasi.findMany.totalPages = res.data.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load pegawai:", res.data?.message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Failed to load pegawai:", res.data?.message);
|
||||||
|
}
|
||||||
programInovasi.findMany.data = [];
|
programInovasi.findMany.data = [];
|
||||||
programInovasi.findMany.total = 0;
|
programInovasi.findMany.total = 0;
|
||||||
programInovasi.findMany.totalPages = 1;
|
programInovasi.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading pegawai:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error loading pegawai:", error);
|
||||||
|
}
|
||||||
programInovasi.findMany.data = [];
|
programInovasi.findMany.data = [];
|
||||||
programInovasi.findMany.total = 0;
|
programInovasi.findMany.total = 0;
|
||||||
programInovasi.findMany.totalPages = 1;
|
programInovasi.findMany.totalPages = 1;
|
||||||
@@ -112,19 +121,25 @@ const programInovasi = proxy({
|
|||||||
image: true;
|
image: true;
|
||||||
};
|
};
|
||||||
}> | null,
|
}> | null,
|
||||||
|
loading: false,
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/landingpage/programinovasi/${id}`);
|
programInovasi.findUnique.loading = true;
|
||||||
if (res.ok) {
|
const res = await (ApiFetch.api.landingpage.programinovasi as any)[id].get();
|
||||||
const data = await res.json();
|
if (res.data?.success) {
|
||||||
programInovasi.findUnique.data = data.data ?? null;
|
programInovasi.findUnique.data = res.data.data ?? null;
|
||||||
|
return res.data.data;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to fetch program inovasi:", res.statusText);
|
toast.error(res.data?.message || "Gagal memuat data program inovasi");
|
||||||
programInovasi.findUnique.data = null;
|
programInovasi.findUnique.data = null;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching program inovasi:", error);
|
console.error("Error fetching program inovasi:", error);
|
||||||
programInovasi.findUnique.data = null;
|
programInovasi.findUnique.data = null;
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
programInovasi.findUnique.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -135,27 +150,18 @@ const programInovasi = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
programInovasi.delete.loading = true;
|
programInovasi.delete.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.programinovasi as any)["del"][id].delete();
|
||||||
|
|
||||||
const response = await fetch(
|
if (res.data?.success) {
|
||||||
`/api/landingpage/programinovasi/del/${id}`,
|
toast.success(res.data.message || "Program inovasi berhasil dihapus");
|
||||||
{
|
await programInovasi.findMany.load();
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && result?.success) {
|
|
||||||
toast.success(result.message || "Program inovasi berhasil dihapus");
|
|
||||||
await programInovasi.findMany.load(); // refresh list
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus program inovasi");
|
toast.error(res.data?.message || "Gagal menghapus program inovasi");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal delete:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
}
|
||||||
toast.error("Terjadi kesalahan saat menghapus program inovasi");
|
toast.error("Terjadi kesalahan saat menghapus program inovasi");
|
||||||
} finally {
|
} finally {
|
||||||
programInovasi.delete.loading = false;
|
programInovasi.delete.loading = false;
|
||||||
@@ -174,20 +180,11 @@ const programInovasi = proxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/landingpage/programinovasi/${id}`, {
|
programInovasi.update.loading = true;
|
||||||
method: "GET",
|
const res = await (ApiFetch.api.landingpage.programinovasi as any)[id].get();
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
if (res.data?.success) {
|
||||||
|
const data = res.data.data;
|
||||||
if (result?.success) {
|
|
||||||
const data = result.data;
|
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -197,13 +194,15 @@ const programInovasi = proxy({
|
|||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
toast.error(res.data?.message || "Gagal mengambil data program inovasi");
|
||||||
result?.message || "Gagal mengambil data program inovasi"
|
return null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error loading program inovasi:", error);
|
||||||
|
}
|
||||||
toast.error("Terjadi kesalahan saat mengambil data program inovasi");
|
toast.error("Terjadi kesalahan saat mengambil data program inovasi");
|
||||||
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
programInovasi.update.loading = false;
|
programInovasi.update.loading = false;
|
||||||
}
|
}
|
||||||
@@ -221,41 +220,25 @@ const programInovasi = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
programInovasi.update.loading = true;
|
programInovasi.update.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.programinovasi as any)[this.id].put({
|
||||||
|
name: this.form.name,
|
||||||
|
description: this.form.description,
|
||||||
|
imageId: this.form.imageId,
|
||||||
|
link: this.form.link,
|
||||||
|
});
|
||||||
|
|
||||||
const response = await fetch(
|
if (res.data?.success) {
|
||||||
`/api/landingpage/programinovasi/${this.id}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: this.form.name,
|
|
||||||
description: this.form.description,
|
|
||||||
imageId: this.form.imageId,
|
|
||||||
link: this.form.link,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(
|
|
||||||
errorData.message || `HTTP error! status: ${response.status}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
toast.success("Berhasil update program inovasi");
|
toast.success("Berhasil update program inovasi");
|
||||||
await programInovasi.findMany.load(); // refresh list
|
await programInovasi.findMany.load();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || "Gagal update program inovasi");
|
toast.error(res.data?.message || "Gagal update program inovasi");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating program inovasi:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error updating program inovasi:", error);
|
||||||
|
}
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
@@ -443,7 +426,7 @@ 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().nullable().optional(),
|
imageId: z.string().nullable().optional(),
|
||||||
iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
|
iconUrl: z.string().optional(), // ✅ Optional - tidak selalu required
|
||||||
icon: z.string().nullable().optional(),
|
icon: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -484,10 +467,15 @@ const mediaSosial = proxy({
|
|||||||
mediaSosial.findMany.load();
|
mediaSosial.findMany.load();
|
||||||
return toast.success("Sukses menambahkan");
|
return toast.success("Sukses menambahkan");
|
||||||
}
|
}
|
||||||
console.log(res);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log(res);
|
||||||
|
}
|
||||||
return toast.error("failed create");
|
return toast.error("failed create");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log((error as Error).message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log((error as Error).message);
|
||||||
|
}
|
||||||
|
toast.error("Gagal menambahkan data");
|
||||||
} finally {
|
} finally {
|
||||||
mediaSosial.create.loading = false;
|
mediaSosial.create.loading = false;
|
||||||
}
|
}
|
||||||
@@ -518,13 +506,17 @@ const mediaSosial = proxy({
|
|||||||
mediaSosial.findMany.total = res.data.total || 0;
|
mediaSosial.findMany.total = res.data.total || 0;
|
||||||
mediaSosial.findMany.totalPages = res.data.totalPages || 1;
|
mediaSosial.findMany.totalPages = res.data.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load media sosial:", res.data?.message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Failed to load media sosial:", res.data?.message);
|
||||||
|
}
|
||||||
mediaSosial.findMany.data = [];
|
mediaSosial.findMany.data = [];
|
||||||
mediaSosial.findMany.total = 0;
|
mediaSosial.findMany.total = 0;
|
||||||
mediaSosial.findMany.totalPages = 1;
|
mediaSosial.findMany.totalPages = 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading media sosial:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error loading media sosial:", error);
|
||||||
|
}
|
||||||
mediaSosial.findMany.data = [];
|
mediaSosial.findMany.data = [];
|
||||||
mediaSosial.findMany.total = 0;
|
mediaSosial.findMany.total = 0;
|
||||||
mediaSosial.findMany.totalPages = 1;
|
mediaSosial.findMany.totalPages = 1;
|
||||||
@@ -539,25 +531,32 @@ const mediaSosial = proxy({
|
|||||||
image: true;
|
image: true;
|
||||||
};
|
};
|
||||||
}> | null,
|
}> | null,
|
||||||
|
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;
|
mediaSosial.findUnique.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/landingpage/mediasosial/${id}`);
|
const res = await (ApiFetch.api.landingpage.mediasosial as any)[id].get();
|
||||||
if (res.ok) {
|
if (res.data?.success) {
|
||||||
const data = await res.json();
|
mediaSosial.findUnique.data = res.data.data ?? null;
|
||||||
mediaSosial.findUnique.data = data.data ?? null;
|
return res.data.data;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to fetch media sosial:", res.statusText);
|
toast.error(res.data?.message || "Gagal memuat data media sosial");
|
||||||
mediaSosial.findUnique.data = null;
|
mediaSosial.findUnique.data = null;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching media sosial:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error fetching media sosial:", error);
|
||||||
|
}
|
||||||
mediaSosial.findUnique.data = null;
|
mediaSosial.findUnique.data = null;
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
mediaSosial.findUnique.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -568,24 +567,18 @@ const mediaSosial = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
mediaSosial.delete.loading = true;
|
mediaSosial.delete.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.mediasosial as any)["del"][id].delete();
|
||||||
|
|
||||||
const response = await fetch(`/api/landingpage/mediasosial/del/${id}`, {
|
if (res.data?.success) {
|
||||||
method: "DELETE",
|
toast.success(res.data.message || "Media Sosial berhasil dihapus");
|
||||||
headers: {
|
await mediaSosial.findMany.load();
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && result?.success) {
|
|
||||||
toast.success(result.message || "Media Sosial berhasil dihapus");
|
|
||||||
await mediaSosial.findMany.load(); // refresh list
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus media sosial");
|
toast.error(res.data?.message || "Gagal menghapus media sosial");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Gagal delete:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
}
|
||||||
toast.error("Terjadi kesalahan saat menghapus media sosial");
|
toast.error("Terjadi kesalahan saat menghapus media sosial");
|
||||||
} finally {
|
} finally {
|
||||||
mediaSosial.delete.loading = false;
|
mediaSosial.delete.loading = false;
|
||||||
@@ -603,43 +596,32 @@ const mediaSosial = proxy({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal
|
mediaSosial.update.loading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/landingpage/mediasosial/${id}`, {
|
const res = await (ApiFetch.api.landingpage.mediasosial as any)[id].get();
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (res.data?.success) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
const data = res.data.data;
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result?.success) {
|
|
||||||
const data = result.data;
|
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
name: data.name || "",
|
name: data.name || "",
|
||||||
imageId: data.imageId || null,
|
imageId: data.imageId || null,
|
||||||
iconUrl: data.iconUrl || "",
|
iconUrl: data.iconUrl || "",
|
||||||
icon: data.icon || null,
|
icon: data.icon || null,
|
||||||
|
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
toast.error(res.data?.message || "Gagal mengambil data media sosial");
|
||||||
result?.message || "Gagal mengambil data media sosial"
|
return null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error loading media sosial:", error);
|
||||||
|
}
|
||||||
toast.error("Terjadi kesalahan saat mengambil data media sosial");
|
toast.error("Terjadi kesalahan saat mengambil data media sosial");
|
||||||
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error
|
mediaSosial.update.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -655,41 +637,25 @@ const mediaSosial = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
mediaSosial.update.loading = true;
|
mediaSosial.update.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.mediasosial as any)[this.id].put({
|
||||||
|
name: this.form.name,
|
||||||
|
imageId: this.form.imageId,
|
||||||
|
iconUrl: this.form.iconUrl,
|
||||||
|
icon: this.form.icon,
|
||||||
|
});
|
||||||
|
|
||||||
const response = await fetch(
|
if (res.data?.success) {
|
||||||
`/api/landingpage/mediasosial/${this.id}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: this.form.name,
|
|
||||||
imageId: this.form.imageId,
|
|
||||||
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 (result.success) {
|
|
||||||
toast.success("Berhasil update media sosial");
|
toast.success("Berhasil update media sosial");
|
||||||
await mediaSosial.findMany.load(); // refresh list
|
await mediaSosial.findMany.load();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || "Gagal update media sosial");
|
toast.error(res.data?.message || "Gagal update media sosial");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating media sosial:", error);
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error("Error updating media sosial:", error);
|
||||||
|
}
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function Page() {
|
|||||||
fz={{ base: 'md', md: 'lg' }}
|
fz={{ base: 'md', md: 'lg' }}
|
||||||
lh={{ base: 1.4, md: 1.4 }}
|
lh={{ base: 1.4, md: 1.4 }}
|
||||||
>
|
>
|
||||||
I.B. Surya Prabhawa Manuaba, S.H., M.H.
|
{perbekel.nama || "I.B. Surya Prabhawa Manuaba, S.H., M.H."}
|
||||||
</Text>
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function EditAPBDes() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
// Check if form is valid
|
// Check if form is valid
|
||||||
const isFormValid = () => {
|
const isFormValid = () => {
|
||||||
return (
|
return (
|
||||||
@@ -76,33 +76,62 @@ function EditAPBDes() {
|
|||||||
tipe: 'pendapatan',
|
tipe: 'pendapatan',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Type for the API response
|
// Simpan data original untuk reset form
|
||||||
interface APBDesResponse {
|
const [originalData, setOriginalData] = useState({
|
||||||
id: string;
|
tahun: 0,
|
||||||
image?: {
|
imageId: '',
|
||||||
link: string;
|
fileId: '',
|
||||||
id: string;
|
imageUrl: '',
|
||||||
};
|
fileUrl: '',
|
||||||
file?: {
|
});
|
||||||
link: string;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
// Add other properties as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load data saat pertama kali
|
// Load data saat pertama kali
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (id) {
|
if (!id) return;
|
||||||
apbdesState.edit.load(id).then((response) => {
|
|
||||||
const data = response as unknown as APBDesResponse;
|
const loadData = async () => {
|
||||||
if (data) {
|
try {
|
||||||
// ✅ Ambil link langsung dari response
|
const data = await apbdesState.edit.load(id);
|
||||||
setPreviewImage(data.image?.link || null);
|
|
||||||
setPreviewDoc(data.file?.link || null);
|
if (!data) return;
|
||||||
}
|
|
||||||
});
|
// Set preview dari data lama
|
||||||
}
|
setPreviewImage(data.image?.link || null);
|
||||||
|
setPreviewDoc(data.file?.link || null);
|
||||||
|
|
||||||
|
// Simpan data original untuk reset
|
||||||
|
setOriginalData({
|
||||||
|
tahun: data.tahun || new Date().getFullYear(),
|
||||||
|
imageId: data.imageId || '',
|
||||||
|
fileId: data.fileId || '',
|
||||||
|
imageUrl: data.image?.link || '',
|
||||||
|
fileUrl: data.file?.link || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set form dengan data lama (termasuk imageId dan fileId)
|
||||||
|
apbdesState.edit.form = {
|
||||||
|
tahun: data.tahun || new Date().getFullYear(),
|
||||||
|
imageId: data.imageId || '',
|
||||||
|
fileId: data.fileId || '',
|
||||||
|
items: (data.items || []).map((item: any) => ({
|
||||||
|
kode: item.kode,
|
||||||
|
uraian: item.uraian,
|
||||||
|
anggaran: item.anggaran,
|
||||||
|
realisasi: item.realisasi,
|
||||||
|
selisih: item.selisih,
|
||||||
|
persentase: item.persentase,
|
||||||
|
level: item.level,
|
||||||
|
tipe: item.tipe || 'pendapatan',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading APBDes:', error);
|
||||||
|
toast.error('Gagal memuat data APBDes');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleDrop = (fileType: 'image' | 'doc') => (files: File[]) => {
|
const handleDrop = (fileType: 'image' | 'doc') => (files: File[]) => {
|
||||||
@@ -162,23 +191,38 @@ function EditAPBDes() {
|
|||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
// Upload file baru jika ada
|
// Upload file baru jika ada perubahan
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
|
// Hapus file lama dari form jika ada file baru
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file: imageFile,
|
file: imageFile,
|
||||||
name: imageFile.name,
|
name: imageFile.name,
|
||||||
});
|
});
|
||||||
const imageId = res.data?.data?.id;
|
const imageId = res.data?.data?.id;
|
||||||
if (imageId) apbdesState.edit.form.imageId = imageId;
|
if (imageId) {
|
||||||
|
apbdesState.edit.form.imageId = imageId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (docFile) {
|
if (docFile) {
|
||||||
|
// Hapus file lama dari form jika ada file baru
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file: docFile,
|
file: docFile,
|
||||||
name: docFile.name,
|
name: docFile.name,
|
||||||
});
|
});
|
||||||
const fileId = res.data?.data?.id;
|
const fileId = res.data?.data?.id;
|
||||||
if (fileId) apbdesState.edit.form.fileId = fileId;
|
if (fileId) {
|
||||||
|
apbdesState.edit.form.fileId = fileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak ada file baru, gunakan ID lama (sudah ada di form)
|
||||||
|
// Pastikan imageId dan fileId tetap ada
|
||||||
|
if (!apbdesState.edit.form.imageId) {
|
||||||
|
return toast.warn('Gambar wajib diunggah');
|
||||||
|
}
|
||||||
|
if (!apbdesState.edit.form.fileId) {
|
||||||
|
return toast.warn('Dokumen wajib diunggah');
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await apbdesState.edit.update();
|
const success = await apbdesState.edit.update();
|
||||||
@@ -194,21 +238,33 @@ function EditAPBDes() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
const id = params?.id as string;
|
// Reset ke data original (tahun, imageId, fileId)
|
||||||
if (id) {
|
apbdesState.edit.form = {
|
||||||
apbdesState.edit.load(id);
|
tahun: originalData.tahun,
|
||||||
setImageFile(null);
|
imageId: originalData.imageId,
|
||||||
setDocFile(null);
|
fileId: originalData.fileId,
|
||||||
setNewItem({
|
items: [...apbdesState.edit.form.items], // keep existing items
|
||||||
kode: '',
|
};
|
||||||
uraian: '',
|
|
||||||
anggaran: 0,
|
// Reset preview ke data original
|
||||||
realisasi: 0,
|
setPreviewImage(originalData.imageUrl || null);
|
||||||
level: 1,
|
setPreviewDoc(originalData.fileUrl || null);
|
||||||
tipe: 'pendapatan',
|
|
||||||
});
|
// Reset file uploads
|
||||||
toast.info('Form dikembalikan ke data awal');
|
setImageFile(null);
|
||||||
}
|
setDocFile(null);
|
||||||
|
|
||||||
|
// Reset new item form
|
||||||
|
setNewItem({
|
||||||
|
kode: '',
|
||||||
|
uraian: '',
|
||||||
|
anggaran: 0,
|
||||||
|
realisasi: 0,
|
||||||
|
level: 1,
|
||||||
|
tipe: 'pendapatan',
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.info('Form dikembalikan ke data awal');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,6 +8,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 DOMPurify from 'dompurify';
|
||||||
|
|
||||||
function DetailProgramInovasi() {
|
function DetailProgramInovasi() {
|
||||||
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
|
const stateProgramInovasi = useProxy(profileLandingPageState.programInovasi)
|
||||||
@@ -85,7 +86,7 @@ function DetailProgramInovasi() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold">Deskripsi</Text>
|
<Text fz="lg" fw="bold">Deskripsi</Text>
|
||||||
<Box pl={5}>
|
<Box pl={5}>
|
||||||
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.description || '-' }}></Text>
|
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data.description || '-') }}></Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 DOMPurify from 'dompurify';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import profileLandingPageState from '../../../_state/landing-page/profile';
|
import profileLandingPageState from '../../../_state/landing-page/profile';
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ function ListProgramInovasi({ search }: { search: string }) {
|
|||||||
<Text fw={500}>{item.name}</Text>
|
<Text fw={500}>{item.name}</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ maxWidth: 250 }}>
|
<TableTd style={{ maxWidth: 250 }}>
|
||||||
<Text fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }}></Text>
|
<Text fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.description || '-') }}></Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ maxWidth: 250 }}>
|
<TableTd style={{ maxWidth: 250 }}>
|
||||||
<Tooltip label="Buka tautan program" position="top" withArrow>
|
<Tooltip label="Buka tautan program" position="top" withArrow>
|
||||||
@@ -144,7 +145,7 @@ function ListProgramInovasi({ search }: { search: string }) {
|
|||||||
{/* Description */}
|
{/* Description */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
<Text fz="sm" fw={600} lh={1.4}>Deskripsi</Text>
|
||||||
<Text dangerouslySetInnerHTML={{ __html: item.description || '-' }} fz="sm" c="gray.7" lineClamp={2} />
|
<Text dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.description || '-') }} fz="sm" c="gray.7" lineClamp={2} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Link */}
|
{/* Link */}
|
||||||
|
|||||||
@@ -82,17 +82,17 @@ function EditDaftarInformasiPublik() {
|
|||||||
await daftarInformasi.edit.update();
|
await daftarInformasi.edit.update();
|
||||||
router.push('/admin/ppid/daftar-informasi-publik');
|
router.push('/admin/ppid/daftar-informasi-publik');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating berita:', error);
|
console.error('Error updating daftar informasi:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui berita');
|
toast.error('Terjadi kesalahan saat memperbarui daftar informasi');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
<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 Daftar Informasi Publik
|
Edit Daftar Informasi Publik
|
||||||
</Title>
|
</Title>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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';
|
||||||
import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum';
|
import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -68,7 +69,7 @@ function Page() {
|
|||||||
lh={{ base: 1.15, md: 1.1 }}
|
lh={{ base: 1.15, md: 1.1 }}
|
||||||
fw="bold"
|
fw="bold"
|
||||||
c={colors['blue-button']}
|
c={colors['blue-button']}
|
||||||
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listDasarHukum.findById.data.judul) }}
|
||||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||||
/>
|
/>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
@@ -77,7 +78,7 @@ function Page() {
|
|||||||
<Divider my="xl" color={colors['blue-button']} />
|
<Divider my="xl" color={colors['blue-button']} />
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listDasarHukum.findById.data.content) }}
|
||||||
style={{
|
style={{
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
whiteSpace: 'normal',
|
whiteSpace: 'normal',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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';
|
||||||
import stateProfilePPID from '../../_state/ppid/profile_ppid/profile_PPID';
|
import stateProfilePPID from '../../_state/ppid/profile_ppid/profile_PPID';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -114,7 +115,7 @@ function Page() {
|
|||||||
c={colors['blue-button']}
|
c={colors['blue-button']}
|
||||||
lh={1.5}
|
lh={1.5}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
dangerouslySetInnerHTML={{ __html: item.biodata }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.biodata) }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ function Page() {
|
|||||||
c={colors['blue-button']}
|
c={colors['blue-button']}
|
||||||
lh={1.5}
|
lh={1.5}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
dangerouslySetInnerHTML={{ __html: item.riwayat }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.riwayat) }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -145,7 +146,7 @@ function Page() {
|
|||||||
c={colors['blue-button']}
|
c={colors['blue-button']}
|
||||||
lh={1.5}
|
lh={1.5}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
dangerouslySetInnerHTML={{ __html: item.pengalaman }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.pengalaman) }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -161,7 +162,7 @@ function Page() {
|
|||||||
c={colors['blue-button']}
|
c={colors['blue-button']}
|
||||||
lh={1.5}
|
lh={1.5}
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
dangerouslySetInnerHTML={{ __html: item.unggulan }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.unggulan) }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useProxy } from 'valtio/utils';
|
|||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
function PosisiOrganisasiPPID() {
|
function PosisiOrganisasiPPID() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -100,7 +101,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
|||||||
<Text fz="md" fw={600} lh={1.5} truncate="end" lineClamp={1}>{item.nama}</Text>
|
<Text fz="md" fw={600} lh={1.5} truncate="end" lineClamp={1}>{item.nama}</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd w={200}>
|
<TableTd w={200}>
|
||||||
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
|
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.deskripsi || '-') }} />
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Text fz="md" lh={1.5}>{item.hierarki || '-'}</Text>
|
<Text fz="md" lh={1.5}>{item.hierarki || '-'}</Text>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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';
|
||||||
import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID';
|
import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID';
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
function VisiMisiPPIDList() {
|
function VisiMisiPPIDList() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -96,7 +97,7 @@ function VisiMisiPPIDList() {
|
|||||||
</Title>
|
</Title>
|
||||||
<Text
|
<Text
|
||||||
ta={{ base: "center", md: "justify" }}
|
ta={{ base: "center", md: "justify" }}
|
||||||
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listVisiMisi.findById.data.visi) }}
|
||||||
style={{
|
style={{
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
whiteSpace: 'normal',
|
whiteSpace: 'normal',
|
||||||
@@ -121,7 +122,7 @@ function VisiMisiPPIDList() {
|
|||||||
</Title>
|
</Title>
|
||||||
<Text
|
<Text
|
||||||
ta={"justify"}
|
ta={"justify"}
|
||||||
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }}
|
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listVisiMisi.findById.data.misi) }}
|
||||||
style={{
|
style={{
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
whiteSpace: 'normal',
|
whiteSpace: 'normal',
|
||||||
|
|||||||
@@ -354,7 +354,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
borderLeft: `2px solid ${tokens.colors.primary}`,
|
borderLeft: `2px solid ${tokens.colors.primary}`,
|
||||||
}),
|
}),
|
||||||
...(mounted && isChildActive && !isDark && {
|
...(mounted && isChildActive && !isDark && {
|
||||||
backgroundColor: tokens.colors.bg.hover,
|
backgroundColor: 'rgba(25, 113, 194, 0.1)',
|
||||||
|
borderLeft: `2px solid ${tokens.colors.primary}`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { requireAuth } from "@/lib/api-auth";
|
||||||
|
|
||||||
|
export default async function sejarahDesaFindFirst(request: Request) {
|
||||||
|
// ✅ Authentication check
|
||||||
|
const headers = new Headers(request.url);
|
||||||
|
const authResult = await requireAuth({ headers });
|
||||||
|
if (!authResult.authenticated) {
|
||||||
|
return authResult.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the first active record
|
||||||
|
const data = await prisma.sejarahDesa.findFirst({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
deletedAt: null
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'asc' } // Get the oldest one first
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
message: "Data tidak ditemukan",
|
||||||
|
}, {status: 404})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
}, {status: 200})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal mengambil data sejarah desa:", error)
|
||||||
|
return Response.json({
|
||||||
|
success: false,
|
||||||
|
message: "Terjadi kesalahan saat mengambil data",
|
||||||
|
}, {status: 500})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import Elysia, { t } from "elysia";
|
import Elysia, { t } from "elysia";
|
||||||
import sejarahDesaFindById from "./find-by-id";
|
import sejarahDesaFindById from "./find-by-id";
|
||||||
import sejarahDesaUpdate from "./update";
|
import sejarahDesaUpdate from "./update";
|
||||||
|
import sejarahDesaFindFirst from "./find-first";
|
||||||
|
|
||||||
const SejarahDesa = new Elysia({
|
const SejarahDesa = new Elysia({
|
||||||
prefix: "/sejarah",
|
prefix: "/sejarah",
|
||||||
tags: ["Desa/Profile"],
|
tags: ["Desa/Profile"],
|
||||||
})
|
})
|
||||||
|
.get("/first", async (context) => {
|
||||||
|
const response = await sejarahDesaFindFirst(new Request(context.request));
|
||||||
|
return response;
|
||||||
|
})
|
||||||
.get("/:id", async (context) => {
|
.get("/:id", async (context) => {
|
||||||
const response = await sejarahDesaFindById(new Request(context.request));
|
const response = await sejarahDesaFindById(new Request(context.request));
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { requireAuth } from "@/lib/api-auth";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function sejarahDesaUpdate(context: Context) {
|
export default async function sejarahDesaUpdate(context: Context) {
|
||||||
|
// ✅ Authentication check
|
||||||
|
const authResult = await requireAuth(context);
|
||||||
|
if (!authResult.authenticated) {
|
||||||
|
return authResult.response;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const id = context.params?.id as string;
|
const id = context.params?.id as string;
|
||||||
const body = await context.body as {
|
const body = await context.body as {
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import {
|
|||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text
|
||||||
useMantineColorScheme
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
@@ -24,8 +23,6 @@ type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: tr
|
|||||||
|
|
||||||
function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
||||||
const router = useTransitionRouter();
|
const router = useTransitionRouter();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const isDark = colorScheme === "dark";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div whileHover={{ scale: 1.03 }}>
|
<motion.div whileHover={{ scale: 1.03 }}>
|
||||||
@@ -37,7 +34,7 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) {
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="cursor-pointer transition-all"
|
className="cursor-pointer transition-all"
|
||||||
bg={isDark ? "dark.6" : "white"}
|
bg="white"
|
||||||
>
|
>
|
||||||
<Center h={160}>
|
<Center h={160}>
|
||||||
{data.image?.link ? (
|
{data.image?.link ? (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import { Box, Space, Stack } from "@mantine/core";
|
import { Box, Space, Stack } from "@mantine/core";
|
||||||
|
|
||||||
@@ -5,21 +7,20 @@ import { Navbar } from "@/app/darmasaba/_com/Navbar";
|
|||||||
import Footer from "./_com/Footer";
|
import Footer from "./_com/Footer";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Stack gap={0} bg={colors.grey[1]}>
|
<Stack gap={0} bg={colors.grey[1]}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<Space h={{
|
<Space h={{
|
||||||
base: "3.9rem",
|
base: "3.9rem",
|
||||||
md: "2.5rem"
|
md: "2.5rem"
|
||||||
}} />
|
}} />
|
||||||
<Box style={{
|
<Box style={{
|
||||||
overflow: "scroll"
|
overflow: "scroll"
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -98,16 +98,16 @@ export default function RootLayout({
|
|||||||
<html lang="id" {...mantineHtmlProps}>
|
<html lang="id" {...mantineHtmlProps}>
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<ColorSchemeScript />
|
<ColorSchemeScript defaultColorScheme="light" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<MantineProvider theme={theme}>
|
<MantineProvider theme={theme} defaultColorScheme="light">
|
||||||
{children}
|
{children}
|
||||||
<LoadDataFirstClient />
|
<LoadDataFirstClient />
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
position="bottom-center"
|
position="bottom-center"
|
||||||
hideProgressBar
|
hideProgressBar
|
||||||
style={{ zIndex: 9999 }}
|
style={{ zIndex: 9999 }}
|
||||||
/>
|
/>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
84
src/lib/api-auth.ts
Normal file
84
src/lib/api-auth.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Authentication helper untuk API endpoints
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { requireAuth } from "@/lib/api-auth";
|
||||||
|
*
|
||||||
|
* export default async function myEndpoint(context: Context) {
|
||||||
|
* const authResult = await requireAuth(context);
|
||||||
|
* if (!authResult.authenticated) {
|
||||||
|
* return authResult.response;
|
||||||
|
* }
|
||||||
|
* // Lanjut proses dengan authResult.user
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getSession } from "@/lib/session";
|
||||||
|
|
||||||
|
export type AuthResult =
|
||||||
|
| { authenticated: true; user: any }
|
||||||
|
| { authenticated: false; response: Response };
|
||||||
|
|
||||||
|
export async function requireAuth(context: any): Promise<AuthResult> {
|
||||||
|
try {
|
||||||
|
// Cek session dari cookies
|
||||||
|
const session = await getSession();
|
||||||
|
|
||||||
|
if (!session || !session.user) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
response: new Response(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Unauthorized - Silakan login terlebih dahulu"
|
||||||
|
}), {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check jika user masih aktif
|
||||||
|
if (!session.user.isActive) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
response: new Response(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Akun Anda tidak aktif. Hubungi administrator."
|
||||||
|
}), {
|
||||||
|
status: 403,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated: true,
|
||||||
|
user: session.user
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Auth error:", error);
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
response: new Response(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: "Authentication error"
|
||||||
|
}), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional auth - tidak error jika tidak authenticated
|
||||||
|
* Berguna untuk endpoint yang bisa diakses public atau private
|
||||||
|
*/
|
||||||
|
export async function optionalAuth(context: any): Promise<any> {
|
||||||
|
try {
|
||||||
|
const session = await getSession();
|
||||||
|
return session?.user || null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/lib/session.ts
Normal file
68
src/lib/session.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Session helper menggunakan iron-session
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { getSession } from "@/lib/session";
|
||||||
|
*
|
||||||
|
* const session = await getSession();
|
||||||
|
* if (session?.user) {
|
||||||
|
* // User authenticated
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getIronSession } from 'iron-session';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
|
export type SessionData = {
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
roleId: number;
|
||||||
|
menuIds?: string[] | null;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Session = SessionData & {
|
||||||
|
save: () => Promise<void>;
|
||||||
|
destroy: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SESSION_OPTIONS = {
|
||||||
|
cookieName: 'desa-session',
|
||||||
|
password: process.env.SESSION_PASSWORD || 'default-password-change-in-production',
|
||||||
|
cookieOptions: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax' as const,
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getSession(): Promise<SessionData | null> {
|
||||||
|
try {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const session = await getIronSession<SessionData>(
|
||||||
|
cookieStore,
|
||||||
|
SESSION_OPTIONS
|
||||||
|
);
|
||||||
|
|
||||||
|
return session;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Session error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function destroySession(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const session = await getIronSession<SessionData>(
|
||||||
|
cookieStore,
|
||||||
|
SESSION_OPTIONS
|
||||||
|
);
|
||||||
|
await session.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Destroy session error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,17 +21,18 @@ import { proxy, useSnapshot } from 'valtio';
|
|||||||
|
|
||||||
const STORAGE_KEY = 'darmasaba-admin-dark-mode';
|
const STORAGE_KEY = 'darmasaba-admin-dark-mode';
|
||||||
|
|
||||||
// Initialize from localStorage or system preference
|
// Initialize from localStorage or default to light mode
|
||||||
const getInitialDarkMode = (): boolean => {
|
const getInitialDarkMode = (): boolean => {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
if (stored !== null) {
|
if (stored !== null) {
|
||||||
return stored === 'true';
|
return stored === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to system preference
|
// Default to light mode for first-time users
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
// System preference is NOT used as default to ensure consistent UX
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DarkModeStore {
|
class DarkModeStore {
|
||||||
|
|||||||
Reference in New Issue
Block a user