diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3a391918..c6c47265 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -95,8 +95,7 @@ model FileStorage { SDGSDesa SDGSDesa[] APBDesImage APBDes[] @relation("APBDesImage") APBDesFile APBDes[] @relation("APBDesFile") - - PrestasiDesa PrestasiDesa[] + PrestasiDesa PrestasiDesa[] } //========================================= MENU LANDING PAGE ========================================= // @@ -216,6 +215,81 @@ model KategoriPrestasiDesa { PrestasiDesa PrestasiDesa[] } +//========================================= INDEKS KEPUASAN MASYARAKAT ========================================= // +// Entitas Survey +model Survey { + id String @id @default(cuid()) + title String // Judul survei + totalRespondents Int // Total jumlah responden + averageScore Float // Rata-rata skor + monthlyStats MonthlyStat[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) +} + +// Entitas Statistik Bulanan +model MonthlyStat { + id String @id @default(cuid()) + month String // Nama bulan (e.g., "Januari", "Februari") + respondentsCount Int // Jumlah responden per bulan + surveyId String @unique(map: "monthly_stat_survey_id_month_key") + survey Survey @relation(fields: [surveyId], references: [id]) + AgeStat AgeStat[] + ResponseStat ResponseStat[] + genderStat genderStat[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) +} + +// Entitas Gender + +model genderStat { + id String @id @default(cuid()) + laki Int + perempuan Int + percentLaki Float + percentPerempuan Float + total Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) + MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id]) + monthlyStatId String? +} + +// Entitas Age + +model AgeStat { + id String @id @default(cuid()) + group String // "18-44", "45+" dll + count Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) + MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id]) + monthlyStatId String? +} + +// Entitas Response + +model ResponseStat { + id String @id @default(cuid()) + label String // BAIK / BURUK + count Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) + MonthlyStat MonthlyStat? @relation(fields: [monthlyStatId], references: [id]) + monthlyStatId String? +} + //========================================= MENU PPID ========================================= // //========================================= STRUKTUR PPID ========================================= // diff --git a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts index 7d279cbc..12f09393 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts @@ -23,9 +23,7 @@ const apbdes = proxy({ form: { ...defaultapbdesForm }, loading: false, async create() { - const cek = templateapbDesaForm.safeParse( - apbdes.create.form - ); + const cek = templateapbDesaForm.safeParse(apbdes.create.form); if (!cek.success) { const err = `[${cek.error.issues .map((v) => `${v.path.join(".")}`) @@ -34,9 +32,7 @@ const apbdes = proxy({ } try { apbdes.create.loading = true; - const res = await ApiFetch.api.landingpage.apbdes[ - "create" - ].post({ + const res = await ApiFetch.api.landingpage.apbdes["create"].post({ ...apbdes.create.form, }); @@ -63,9 +59,7 @@ const apbdes = proxy({ }> > | null, async load() { - const res = await ApiFetch.api.landingpage.apbdes[ - "find-many" - ].get(); + const res = await ApiFetch.api.landingpage.apbdes["find-many"].get(); if (res.status === 200) { apbdes.findMany.data = res.data?.data ?? []; } @@ -102,15 +96,12 @@ const apbdes = proxy({ try { apbdes.delete.loading = true; - const response = await fetch( - `/api/landingpage/apbdes/del/${id}`, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - } - ); + const response = await fetch(`/api/landingpage/apbdes/del/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); const result = await response.json(); @@ -177,9 +168,7 @@ const apbdes = proxy({ }, async update() { - const cek = templateapbDesaForm.safeParse( - apbdes.edit.form - ); + const cek = templateapbDesaForm.safeParse(apbdes.edit.form); if (!cek.success) { const err = `[${cek.error.issues .map((v) => `${v.path.join(".")}`) @@ -189,21 +178,18 @@ const apbdes = proxy({ try { apbdes.edit.loading = true; - const response = await fetch( - `/api/landingpage/apbdes/${this.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: this.form.name, - jumlah: this.form.jumlah, - imageId: this.form.imageId, - fileId: this.form.fileId, - }), - } - ); + const response = await fetch(`/api/landingpage/apbdes/${this.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: this.form.name, + jumlah: this.form.jumlah, + imageId: this.form.imageId, + fileId: this.form.fileId, + }), + }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error( @@ -216,16 +202,12 @@ const apbdes = proxy({ await apbdes.findMany.load(); // refresh list return true; } else { - throw new Error( - result.message || "Gagal mengupdate apbdes" - ); + throw new Error(result.message || "Gagal mengupdate apbdes"); } } catch (error) { console.error("Error updating apbdes:", error); toast.error( - error instanceof Error - ? error.message - : "Gagal mengupdate apbdes" + error instanceof Error ? error.message : "Gagal mengupdate apbdes" ); return false; } finally { @@ -239,4 +221,4 @@ const apbdes = proxy({ }, }); -export default apbdes; \ No newline at end of file +export default apbdes; diff --git a/src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat.ts b/src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat.ts new file mode 100644 index 00000000..f181ad2d --- /dev/null +++ b/src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat.ts @@ -0,0 +1,1180 @@ +import ApiFetch from "@/lib/api-fetch"; +import { Prisma } from "@prisma/client"; +import { toast } from "react-toastify"; +import { proxy } from "valtio"; +import { z } from "zod"; + +const templateSurveyForm = z.object({ + title: z.string().min(1, "Judul wajib diisi"), + totalRespondents: z.number().min(0), + averageScore: z.number().min(0), +}); + +const defaultSurveyForm = { + title: "", + totalRespondents: 0, + averageScore: 0, +}; + +const surveyState = proxy({ + create: { + form: { ...defaultSurveyForm }, + loading: false, + async create() { + const cek = templateSurveyForm.safeParse(surveyState.create.form); + if (!cek.success) return toast.error(cek.error.errors[0].message); + + try { + surveyState.create.loading = true; + const res = + await ApiFetch.api.landingpage.indekskepuasanmasyarakat.survey.create.post( + { ...surveyState.create.form } + ); + + if (res.status === 200) { + toast.success("Data survey berhasil ditambahkan"); + surveyState.findMany.load(); + } else { + toast.error("Gagal menambahkan data survey"); + } + } catch (error) { + toast.error("Terjadi kesalahan saat membuat survey"); + console.error(error); + } finally { + surveyState.create.loading = false; + } + }, + }, + + findMany: { + data: null as + | Prisma.SurveyGetPayload<{ + include: { monthlyStats: true }; + }>[] + | null, + loading: false, + async load() { + try { + surveyState.findMany.loading = true; + const res = + await ApiFetch.api.landingpage.indekskepuasanmasyarakat.survey.findMany.get(); + if (res.status === 200) { + surveyState.findMany.data = res.data?.data ?? []; + } + } catch (error) { + toast.error("Gagal mengambil data survey"); + console.error(error); + } finally { + surveyState.findMany.loading = false; + } + }, + }, + + findUnique: { + data: null as Prisma.SurveyGetPayload<{ + include: { monthlyStats: true }; + }> | null, + async load(id: string) { + try { + const res = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/survey/${id}` + ); + if (res.ok) { + const data = await res.json(); + surveyState.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch data", res.status, res.statusText); + surveyState.findUnique.data = null; + } + } catch (error) { + console.error("Error fetching data:", error); + surveyState.findUnique.data = null; + } + }, + }, + + edit: { + id: "", + form: { ...defaultSurveyForm }, + loading: false, + + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + try { + surveyState.edit.loading = true; + + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/survey/${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 = { + title: data.title, + totalRespondents: data.totalRespondents, + averageScore: data.averageScore, + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading survey:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } finally { + surveyState.edit.loading = false; + } + }, + + async update() { + const cek = templateSurveyForm.safeParse(surveyState.edit.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + + try { + surveyState.edit.loading = true; + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/survey/${this.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: this.form.title, + totalRespondents: this.form.totalRespondents, + averageScore: this.form.averageScore, + }), + } + ); + 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 survey"); + await surveyState.findMany.load(); // refresh list + return true; + } else { + throw new Error(result.message || "Gagal mengupdate survey"); + } + } catch (error) { + console.error("Error updating survey:", error); + toast.error( + error instanceof Error ? error.message : "Gagal mengupdate survey" + ); + return false; + } finally { + surveyState.edit.loading = false; + } + }, + reset() { + surveyState.edit.id = ""; + surveyState.edit.form = { ...defaultSurveyForm }; + }, + }, + + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + surveyState.delete.loading = true; + + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/survey/del/${id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + + const result = await response.json(); + + if (response.ok && result?.success) { + toast.success(result.message || "survey berhasil dihapus"); + await surveyState.findMany.load(); // refresh list + } else { + toast.error(result?.message || "Gagal menghapus survey"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus survey"); + } finally { + surveyState.delete.loading = false; + } + }, + }, +}); + +//========================================= MONTHLY STAT ========================================= // + +const templateMonthlyForm = z.object({ + month: z.string().min(1, "Bulan wajib diisi"), + respondentsCount: z.number().min(0, "Jumlah responden minimal 0"), + surveyId: z.string().min(1, "Survey ID wajib diisi"), +}); + +const defaultMonthlyForm = { + month: "", + respondentsCount: 0, + surveyId: "", +}; + +const monthlyStatState = proxy({ + create: { + form: { ...defaultMonthlyForm }, + loading: false, + async create() { + const cek = templateMonthlyForm.safeParse(monthlyStatState.create.form); + if (!cek.success) { + toast.error(cek.error.errors[0].message); + return; + } + + try { + monthlyStatState.create.loading = true; + const res = + await ApiFetch.api.landingpage.indekskepuasanmasyarakat.monthlystat.create.post( + { + ...monthlyStatState.create.form, + } + ); + + if (res.status === 200) { + toast.success("Data statistik bulanan berhasil ditambahkan"); + monthlyStatState.findMany.load(); + } else { + toast.error("Gagal menambahkan data statistik bulanan"); + } + } catch (error) { + console.error(error); + toast.error("Terjadi kesalahan saat membuat data bulanan"); + } finally { + monthlyStatState.create.loading = false; + } + }, + }, + + findMany: { + data: null as + | Prisma.MonthlyStatGetPayload<{ include: { survey: true } }>[] + | null, + loading: false, + async load() { + try { + monthlyStatState.findMany.loading = true; + const res = + await ApiFetch.api.landingpage.indekskepuasanmasyarakat.monthlystat.findMany.get(); + if (res.status === 200) { + monthlyStatState.findMany.data = res.data?.data ?? []; + } + } catch (error) { + console.error(error); + toast.error("Gagal mengambil data statistik bulanan"); + } finally { + monthlyStatState.findMany.loading = false; + } + }, + }, + + findUnique: { + data: null as Prisma.MonthlyStatGetPayload<{ + include: { survey: true }; + }> | null, + async load(id: string) { + try { + const res = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/monthlystat/${id}` + ); + if (res.ok) { + const data = await res.json(); + monthlyStatState.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch monthlyStat", res.status); + } + } catch (error) { + console.error("Error fetching monthlyStat:", error); + } + }, + }, + + edit: { + id: "", + form: { ...defaultMonthlyForm }, + loading: false, + + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + try { + monthlyStatState.edit.loading = true; + + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/monthlystat/${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 = { + month: data.month, + respondentsCount: data.respondentsCount, + surveyId: data.surveyId, + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading survey:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } finally { + monthlyStatState.edit.loading = false; + } + }, + + async update() { + const cek = templateMonthlyForm.safeParse(monthlyStatState.edit.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + + try { + monthlyStatState.edit.loading = true; + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/monthlystat/${this.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + month: this.form.month, + respondentsCount: this.form.respondentsCount, + surveyId: this.form.surveyId, + }), + } + ); + 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 survey"); + await monthlyStatState.findMany.load(); // refresh list + return true; + } else { + throw new Error(result.message || "Gagal mengupdate survey"); + } + } catch (error) { + console.error("Error updating survey:", error); + toast.error( + error instanceof Error ? error.message : "Gagal mengupdate survey" + ); + return false; + } finally { + monthlyStatState.edit.loading = false; + } + }, + reset() { + monthlyStatState.edit.id = ""; + monthlyStatState.edit.form = { ...defaultMonthlyForm }; + }, + }, + + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + monthlyStatState.delete.loading = true; + const res = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/monthlystat/del/${id}`, + { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + } + ); + + const result = await res.json(); + if (res.ok && result?.success) { + toast.success("Data berhasil dihapus"); + monthlyStatState.findMany.load(); + } else { + toast.error(result.message || "Gagal menghapus data"); + } + } catch (error) { + console.error("Delete error:", error); + toast.error("Terjadi kesalahan saat delete"); + } finally { + monthlyStatState.delete.loading = false; + } + }, + }, +}); + +//========================================= AGE STAT ========================================= // + +const templateAgeStatForm = z.object({ + group: z.string().min(1, "Group wajib diisi"), + count: z.number().min(0, "Jumlah wajib diisi"), + monthlyStatId: z.string().min(1, "Monthly Stat ID wajib diisi"), +}); + +const defaultAgeStatForm = { + group: "", + count: 0, + monthlyStatId: "", +}; + +const ageStatState = proxy({ + create: { + form: { ...defaultAgeStatForm }, + loading: false, + async create() { + const cek = templateAgeStatForm.safeParse(ageStatState.create.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + try { + ageStatState.create.loading = true; + const res = await ApiFetch.api.landingpage.indekskepuasanmasyarakat.age[ + "create" + ].post({ + ...ageStatState.create.form, + }); + + if (res.status === 200) { + ageStatState.findMany.load(); + return toast.success("Data berhasil ditambahkan"); + } + return toast.error("Gagal menambahkan data"); + } catch (error) { + console.log(error); + toast.error("Gagal menambahkan data"); + } finally { + ageStatState.create.loading = false; + } + }, + }, + findMany: { + data: null as Array< + Prisma.AgeStatGetPayload<{ + include: { + MonthlyStat: true; + }; + }> + > | null, + async load() { + const res = await ApiFetch.api.landingpage.indekskepuasanmasyarakat.age[ + "findMany" + ].get(); + if (res.status === 200) { + ageStatState.findMany.data = res.data?.data ?? []; + } + }, + }, + findUnique: { + data: null as Prisma.AgeStatGetPayload<{ + include: { + MonthlyStat: true; + }; + }> | null, + async load(id: string) { + try { + const res = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/age/${id}` + ); + if (res.ok) { + const data = await res.json(); + ageStatState.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch data", res.status, res.statusText); + ageStatState.findUnique.data = null; + } + } catch (error) { + console.error("Error fetching data:", error); + ageStatState.findUnique.data = null; + } + }, + }, + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + ageStatState.delete.loading = true; + + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/age/del/${id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + + const result = await response.json(); + + if (response.ok && result?.success) { + toast.success(result.message || "age berhasil dihapus"); + await ageStatState.findMany.load(); // refresh list + } else { + toast.error(result?.message || "Gagal menghapus age"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus age"); + } finally { + ageStatState.delete.loading = false; + } + }, + }, + edit: { + id: "", + form: { ...defaultAgeStatForm }, + loading: false, + + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + try { + ageStatState.edit.loading = true; + + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/age/${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 = { + group: data.group, + count: data.count, + monthlyStatId: data.monthlyStatId, + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading ages :", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } finally { + ageStatState.edit.loading = false; + } + }, + + async update() { + const cek = templateAgeStatForm.safeParse(ageStatState.edit.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + + try { + ageStatState.edit.loading = true; + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/age/${this.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + group: this.form.group, + count: this.form.count, + monthlyStatId: this.form.monthlyStatId, + }), + } + ); + 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 age"); + await ageStatState.findMany.load(); // refresh list + return true; + } else { + throw new Error(result.message || "Gagal mengupdate age"); + } + } catch (error) { + console.error("Error updating age:", error); + toast.error( + error instanceof Error ? error.message : "Gagal mengupdate age" + ); + return false; + } finally { + ageStatState.edit.loading = false; + } + }, + reset() { + ageStatState.edit.id = ""; + ageStatState.edit.form = { ...defaultAgeStatForm }; + }, + }, +}); + +//========================================= GENDER STAT ========================================= // + +const templateGenderStatForm = z.object({ + laki: z.number().min(0, "Jumlah wajib diisi"), + perempuan: z.number().min(0, "Jumlah wajib diisi"), + monthlyStatId: z.string().min(1, "Monthly Stat ID wajib diisi"), + total: z.number().min(0, "Jumlah wajib diisi"), + percentLaki: z.number().min(0, "Jumlah wajib diisi"), + percentPerempuan: z.number().min(0, "Jumlah wajib diisi"), +}); + +const defaultGenderStatForm = { + laki: 0, + perempuan: 0, + total: 0, + percentLaki: 0, + percentPerempuan: 0, + monthlyStatId: "", +}; + +const genderStatState = proxy({ + create: { + form: { ...defaultGenderStatForm }, + loading: false, + async create() { + const cek = templateGenderStatForm.safeParse(genderStatState.create.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + + const { laki, perempuan } = genderStatState.create.form; + const total = laki + perempuan; + const percentLaki = total > 0 ? Number(((laki / total) * 100).toFixed(2)) : 0; + const percentPerempuan = total > 0 ? Number(((perempuan / total) * 100).toFixed(2)) : 0; + + try { + genderStatState.create.loading = true; + const res = await fetch('/api/landingpage/indekskepuasanmasyarakat/gender/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...genderStatState.create.form, + total, + percentLaki, + percentPerempuan, + }), + }); + + const result = await res.json(); + if (result.success) { + toast.success("Data berhasil ditambahkan"); + genderStatState.findMany.load(); // refresh list + } else { + toast.error(result.message); + } + } catch (err) { + toast.error("Gagal tambah data"); + console.error(err); + } finally { + genderStatState.create.loading = false; + } + }, + }, + findMany: { + data: null as Array< + Prisma.genderStatGetPayload<{ + include: { + MonthlyStat: true; + }; + }> + > | null, + async load() { + const res = + await ApiFetch.api.landingpage.indekskepuasanmasyarakat.gender[ + "findMany" + ].get(); + if (res.status === 200) { + genderStatState.findMany.data = res.data?.data ?? []; + } + }, + }, + findUnique: { + data: null as Prisma.genderStatGetPayload<{ + include: { + MonthlyStat: true; + }; + }> | null, + async load(id: string) { + try { + const res = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/gender/${id}` + ); + if (res.ok) { + const data = await res.json(); + genderStatState.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch data", res.status, res.statusText); + genderStatState.findUnique.data = null; + } + } catch (error) { + console.error("Error fetching data:", error); + genderStatState.findUnique.data = null; + } + }, + }, + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + genderStatState.delete.loading = true; + + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/gender/del/${id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + + const result = await response.json(); + + if (response.ok && result?.success) { + toast.success(result.message || "gender berhasil dihapus"); + await genderStatState.findMany.load(); // refresh list + } else { + toast.error(result?.message || "Gagal menghapus gender"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus gender"); + } finally { + genderStatState.delete.loading = false; + } + }, + }, + edit: { + id: "", + form: { ...defaultGenderStatForm }, + loading: false, + + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + try { + genderStatState.edit.loading = true; + + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/gender/${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 = { + laki: data.laki, + perempuan: data.perempuan, + monthlyStatId: data.monthlyStatId, + total: data.total, + percentLaki: Number(data.percentLaki), + percentPerempuan: Number(data.percentPerempuan), + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading ages :", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } finally { + genderStatState.edit.loading = false; + } + }, + + async update() { + const { laki, perempuan } = genderStatState.edit.form; + const total = laki + perempuan; + const percentLaki = total > 0 ? Number(((laki / total) * 100).toFixed(2)) : 0; + const percentPerempuan = total > 0 ? Number(((perempuan / total) * 100).toFixed(2)) : 0; + + try { + genderStatState.edit.loading = true; + const res = await fetch(`/api/landingpage/indekskepuasanmasyarakat/gender/${genderStatState.edit.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...genderStatState.edit.form, + total, + percentLaki: Number(percentLaki), + percentPerempuan: Number(percentPerempuan), + }), + }); + if (!res.ok) { + const errorData = await res.json().catch(() => ({})); + throw new Error( + errorData.message || `HTTP error! status: ${res.status}` + ); + } + const result = await res.json(); + if (result.success) { + toast.success("Berhasil update gender"); + await genderStatState.findMany.load(); // refresh list + return true; + } else { + throw new Error(result.message || "Gagal mengupdate gender"); + } + } catch (error) { + console.error("Error updating gender:", error); + toast.error( + error instanceof Error ? error.message : "Gagal mengupdate gender" + ); + return false; + } finally { + genderStatState.edit.loading = false; + } + }, + reset() { + genderStatState.edit.id = ""; + genderStatState.edit.form = { ...defaultGenderStatForm }; + }, + }, +}); + +//========================================= RESPONSE STAT ========================================= // + +const templateResponseStatForm = z.object({ + label: z.string().min(1, "Label wajib diisi"), + count: z.number().min(0, "Jumlah wajib diisi"), + monthlyStatId: z.string().min(1, "Monthly Stat ID wajib diisi"), +}); + +const defaultResponseStatForm = { + label: "", + count: 0, + monthlyStatId: "", +}; + +const responseStatState = proxy({ + create: { + form: { ...defaultResponseStatForm }, + loading: false, + async create() { + const cek = templateResponseStatForm.safeParse( + responseStatState.create.form + ); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + try { + responseStatState.create.loading = true; + const res = + await ApiFetch.api.landingpage.indekskepuasanmasyarakat.response[ + "create" + ].post({ + ...responseStatState.create.form, + }); + + if (res.status === 200) { + responseStatState.findMany.load(); + return toast.success("Data berhasil ditambahkan"); + } + return toast.error("Gagal menambahkan data"); + } catch (error) { + console.log(error); + toast.error("Gagal menambahkan data"); + } finally { + responseStatState.create.loading = false; + } + }, + }, + findMany: { + data: null as Array< + Prisma.ResponseStatGetPayload<{ + include: { + MonthlyStat: true; + }; + }> + > | null, + async load() { + const res = + await ApiFetch.api.landingpage.indekskepuasanmasyarakat.response[ + "findMany" + ].get(); + if (res.status === 200) { + responseStatState.findMany.data = res.data?.data ?? []; + } + }, + }, + findUnique: { + data: null as Prisma.ResponseStatGetPayload<{ + include: { + MonthlyStat: true; + }; + }> | null, + async load(id: string) { + try { + const res = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/response/${id}` + ); + if (res.ok) { + const data = await res.json(); + responseStatState.findUnique.data = data.data ?? null; + } else { + console.error("Failed to fetch data", res.status, res.statusText); + responseStatState.findUnique.data = null; + } + } catch (error) { + console.error("Error fetching data:", error); + responseStatState.findUnique.data = null; + } + }, + }, + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + responseStatState.delete.loading = true; + + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/response/del/${id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + + const result = await response.json(); + + if (response.ok && result?.success) { + toast.success(result.message || "response berhasil dihapus"); + await responseStatState.findMany.load(); // refresh list + } else { + toast.error(result?.message || "Gagal menghapus response"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus response"); + } finally { + responseStatState.delete.loading = false; + } + }, + }, + edit: { + id: "", + form: { ...defaultResponseStatForm }, + loading: false, + + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + + try { + responseStatState.edit.loading = true; + + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/response/${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 = { + label: data.label, + count: data.count, + monthlyStatId: data.monthlyStatId, + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading ages :", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } finally { + responseStatState.edit.loading = false; + } + }, + + async update() { + const cek = templateResponseStatForm.safeParse( + responseStatState.edit.form + ); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + + try { + responseStatState.edit.loading = true; + const response = await fetch( + `/api/landingpage/indekskepuasanmasyarakat/response/${this.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + label: this.form.label, + count: this.form.count, + monthlyStatId: this.form.monthlyStatId, + }), + } + ); + 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 response"); + await responseStatState.findMany.load(); // refresh list + return true; + } else { + throw new Error(result.message || "Gagal mengupdate response"); + } + } catch (error) { + console.error("Error updating response:", error); + toast.error( + error instanceof Error ? error.message : "Gagal mengupdate response" + ); + return false; + } finally { + responseStatState.edit.loading = false; + } + }, + reset() { + responseStatState.edit.id = ""; + responseStatState.edit.form = { ...defaultResponseStatForm }; + }, + }, +}); + +const indeksKepuasanState = proxy({ + surveyState, + monthlyStatState, + ageStatState, + genderStatState, + responseStatState, +}); + +export default indeksKepuasanState; diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/_lib/layoutTabs.tsx new file mode 100644 index 00000000..7168a9c4 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/_lib/layoutTabs.tsx @@ -0,0 +1,67 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; + +function LayoutTabs({ children }: { children: React.ReactNode }) { + const router = useRouter() + const pathname = usePathname() + const tabs = [ + { + label: "List Survey", + value: "listSurvey", + href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-survey" + }, + { + label: "List Bulanan", + value: "listBulanan", + href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan" + }, + { + label: "List Gender Stat", + value: "listGenderStat", + href: "/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat" + } + ]; + const curentTab = tabs.find(tab => tab.href === pathname) + const [activeTab, setActiveTab] = useState(curentTab?.value || tabs[0].value); + + const handleTabChange = (value: string | null) => { + const tab = tabs.find(t => t.value === value) + if (tab) { + router.push(tab.href) + } + setActiveTab(value) + } + + useEffect(() => { + const match = tabs.find(tab => tab.href === pathname) + if (match) { + setActiveTab(match.value) + } + }, [pathname]) + + return ( + + Indeks Kepuasan Masyarakat + + + {tabs.map((e, i) => ( + {e.label} + ))} + + {tabs.map((e, i) => ( + + {/* Konten dummy, bisa diganti tergantung routing */} + <> + + ))} + + {children} + + ); +} + +export default LayoutTabs; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/layout.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/layout.tsx new file mode 100644 index 00000000..b42a23fd --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/layout.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import LayoutTabs from './_lib/layoutTabs'; + +function Layout({children} : {children: React.ReactNode}) { + return ( + + {children} + + ); +} + +export default Layout; diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-bulanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-bulanan/[id]/edit/page.tsx new file mode 100644 index 00000000..e155f701 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-bulanan/[id]/edit/page.tsx @@ -0,0 +1,128 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat'; +import colors from '@/con/colors'; +import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } 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 EditListBulanan() { + const editState = useProxy(indeksKepuasanState.monthlyStatState) + const params = useParams() + const router = useRouter() + const [formData, setFormData] = useState({ + month: editState.edit.form.month || '', + respondentsCount: editState.edit.form.respondentsCount || 0, + surveyId: editState.edit.form.surveyId || '', + }) + + useEffect(() => { + const loadSurvey = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await editState.edit.load(id); // akses langsung, bukan dari proxy + if (data) { + setFormData({ + month: data.month, + respondentsCount: data.respondentsCount, + surveyId: data.surveyId, + }); + } + } catch (error) { + console.error("Error loading list bulanan:", error); + toast.error("Gagal memuat data list bulanan"); + } + }; + indeksKepuasanState.surveyState.findMany.load(); + loadSurvey(); + }, [params?.id]); + + const handleSubmit = async () => { + + try { + // edit global state with form data + editState.edit.form = { + ...editState.edit.form, + month: formData.month, + respondentsCount: formData.respondentsCount, + surveyId: formData.surveyId, + }; + + await editState.edit.update(); + toast.success("list bulanan berhasil diperbarui!"); + router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-bulanan"); + } catch (error) { + console.error("Error updating list bulanan:", error); + toast.error("Terjadi kesalahan saat memperbarui list bulanan"); + } + }; + + return ( + + + + + + + Edit List Survey + + + { + setFormData({ + ...formData, + month: val.target.value + }) + }} + label={Bulan} + placeholder='Masukkan bulan' + /> + { + setFormData({ + ...formData, + respondentsCount: Number(val.target.value) + }) + }} + label={Total Responden} + placeholder='Masukkan total responden' + /> + Pilih Survey} + placeholder="Pilih survey" + value={stateCreate.create.form.surveyId} + onChange={(value) => { + if (value) stateCreate.create.form.surveyId = value; + }} + data={ + indeksKepuasanState.surveyState.findMany.data?.map((survey) => ({ + value: survey.id, + label: `${survey.title} (${survey.totalRespondents} responden)`, + })) || [] + } + /> + + + + + + + ); +} + +export default CreateListBulanan; diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-bulanan/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-bulanan/page.tsx new file mode 100644 index 00000000..a6c327c5 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-bulanan/page.tsx @@ -0,0 +1,98 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; +import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat'; +import JudulList from '../../../_com/judulList'; + + + +function ListBulananLandingPage() { + const [search, setSearch] = useState(''); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListBUlanan({ search }: { search: string }) { + const listState = useProxy(indeksKepuasanState.monthlyStatState) + const router = useRouter(); + useEffect(() => { + listState.findMany.load() + }, []) + + const filteredData = (listState.findMany.data || []).filter(item => { + const keyword = search.toLowerCase(); + return ( + item.month.toLowerCase().includes(keyword) || + item.respondentsCount.toString().includes(keyword) + ); + }); + + if (!listState.findMany.data) { + return ( + + + + ) + } + + return ( + + + + + + + + + Bulan + Total Responden + Detail + + + + {filteredData.map((item) => ( + + + + {item.month} + + + + {item.respondentsCount} + + + + + + ))} + +
+
+
+
+
+ ) +} + +export default ListBulananLandingPage; diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/[id]/edit/page.tsx new file mode 100644 index 00000000..fe33b3fb --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/[id]/edit/page.tsx @@ -0,0 +1,186 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat'; +import colors from '@/con/colors'; +import { Box, Button, Group, Paper, Select, Stack, Text, TextInput } 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 EditListSurvey() { + const editState = useProxy(indeksKepuasanState.genderStatState) + const params = useParams() + const router = useRouter() + const [formData, setFormData] = useState({ + laki: editState.edit.form.laki || 0, + perempuan: editState.edit.form.perempuan || 0, + monthlyStatId: editState.edit.form.monthlyStatId || '', + total: editState.edit.form.total || 0, + percentLaki: editState.edit.form.percentLaki || 0, + percentPerempuan: editState.edit.form.percentPerempuan || 0, + }) + + useEffect(() => { + indeksKepuasanState.monthlyStatState.findMany.load(); + const loadSurvey = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await editState.edit.load(id); // akses langsung, bukan dari proxy + if (data) { + setFormData({ + laki: data.laki || 0, + perempuan: data.perempuan || 0, + monthlyStatId: data.monthlyStatId || '', + total: data.total || 0, + percentLaki: data.percentLaki || 0, + percentPerempuan: data.percentPerempuan || 0, + }); + } + } catch (error) { + console.error("Error loading list survey:", error); + toast.error("Gagal memuat data list survey"); + } + }; + loadSurvey(); + }, [params?.id]); + + // Hitung total dan persentase saat nilai laki atau perempuan berubah + useEffect(() => { + const total = formData.laki + formData.perempuan; + const percentLaki = total > 0 ? Math.round((formData.laki / total) * 100) : 0; + const percentPerempuan = 100 - percentLaki; + + setFormData(prev => ({ + ...prev, + total, + percentLaki, + percentPerempuan + })); + }, [formData.laki, formData.perempuan]); + + const handleSubmit = async () => { + if (!formData.monthlyStatId) { + return toast.error("Silakan pilih bulan terlebih dahulu"); + } + + if (formData.laki < 0 || formData.perempuan < 0) { + return toast.error("Nilai tidak boleh negatif"); + } + + try { + // edit global state with form data + editState.edit.form = { + ...editState.edit.form, + laki: formData.laki, + perempuan: formData.perempuan, + monthlyStatId: formData.monthlyStatId, + total: formData.total, + percentLaki: formData.percentLaki, + percentPerempuan: formData.percentPerempuan, + }; + + await editState.edit.update(); + toast.success("Data gender berhasil diperbarui!"); + router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-gender-stat"); + } catch (error) { + console.error("Error updating list gender:", error); + toast.error("Terjadi kesalahan saat memperbarui data gender"); + } + }; + + return ( + + + + + + + Edit List Survey + + + { + setFormData({ + ...formData, + laki: Number(val.target.value) + }) + }} + label={Laki - Laki} + placeholder='Masukkan laki - laki' + /> + { + setFormData({ + ...formData, + perempuan: Number(val.target.value) + }) + }} + label={Perempuan} + placeholder='Masukkan perempuan' + /> + Total} + disabled + /> + Persentase Laki Laki} + disabled + /> + Persentase Perempuan} + disabled + /> + Pilih Bulan} + placeholder="Pilih bulanan" + value={stateCreate.create.form.monthlyStatId} + onChange={(value) => { + if (value) stateCreate.create.form.monthlyStatId = value; + }} + data={ + indeksKepuasanState.monthlyStatState.findMany.data?.map((monthlyStat) => ({ + value: monthlyStat.id, + label: `${monthlyStat.month} (${monthlyStat.respondentsCount} responden)`, + })) || [] + } + /> + + + + + + + ); +} + +export default CreateListGenderStat; diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/page.tsx new file mode 100644 index 00000000..34d595ad --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-gender-stat/page.tsx @@ -0,0 +1,106 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; +import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import JudulList from '../../../_com/judulList'; +import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat'; + + + +function ListGenderStat() { + const [search, setSearch] = useState(''); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListGender({ search }: { search: string }) { + const listState = useProxy(indeksKepuasanState.genderStatState) + const router = useRouter(); + useEffect(() => { + listState.findMany.load() + }, []) + + const filteredData = (listState.findMany.data || []).filter(item => { + const keyword = search.toLowerCase(); + return ( + item.laki.toString().includes(keyword) || + item.perempuan.toString().includes(keyword) + ); + }); + + if (!listState.findMany.data) { + return ( + + + + ) + } + + return ( + + + + + + + + + Laki Laki + Perempuan + Persentase Laki Laki + Persentase Perempuan + Detail + + + + {filteredData.map((item) => ( + + + + {item.laki} + + + + {item.perempuan} + + + {item.percentLaki}% + + + {item.percentPerempuan}% + + + + + + ))} + +
+
+
+
+
+ ) +} + +export default ListGenderStat; diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/[id]/edit/page.tsx new file mode 100644 index 00000000..c12b9195 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/[id]/edit/page.tsx @@ -0,0 +1,122 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat'; +import colors from '@/con/colors'; +import { Box, Button, Group, Paper, Stack, Text, TextInput } 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 EditListSurvey() { + const editState = useProxy(indeksKepuasanState.surveyState) + const params = useParams() + const router = useRouter() + const [formData, setFormData] = useState({ + title: editState.edit.form.title || '', + totalRespondents: editState.edit.form.totalRespondents || 0, + averageScore: editState.edit.form.averageScore || 0, + }) + + useEffect(() => { + const loadSurvey = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await editState.edit.load(id); // akses langsung, bukan dari proxy + if (data) { + setFormData({ + title: data.title || '', + totalRespondents: data.totalRespondents || 0, + averageScore: data.averageScore || 0, + }); + } + } catch (error) { + console.error("Error loading list survey:", error); + toast.error("Gagal memuat data list survey"); + } + }; + + loadSurvey(); + }, [params?.id]); + + const handleSubmit = async () => { + + try { + // edit global state with form data + editState.edit.form = { + ...editState.edit.form, + title: formData.title, + totalRespondents: formData.totalRespondents, + averageScore: formData.averageScore, + }; + + await editState.edit.update(); + toast.success("list survey berhasil diperbarui!"); + router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey"); + } catch (error) { + console.error("Error updating list survey:", error); + toast.error("Terjadi kesalahan saat memperbarui list survey"); + } + }; + + return ( + + + + + + + Edit List Survey + + + { + setFormData({ + ...formData, + title: val.target.value + }) + }} + label={Judul} + placeholder='Masukkan judul' + /> + { + setFormData({ + ...formData, + totalRespondents: Number(val.target.value) + }) + }} + label={Total Responden} + placeholder='Masukkan total responden' + /> + { + setFormData({ + ...formData, + averageScore: Number(val.target.value) + }) + }} + label={Skor Rata-rata} + placeholder='Masukkan skor' + /> + + + + + + + + + ); +} + +export default EditListSurvey; diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/[id]/page.tsx new file mode 100644 index 00000000..df44dde9 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/[id]/page.tsx @@ -0,0 +1,109 @@ +'use client' +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; +import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat'; +import colors from '@/con/colors'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; + + +function DetailKegiatanDesa() { + const detailState = useProxy(indeksKepuasanState.surveyState) + const [modalHapus, setModalHapus] = useState(false) + const [selectedId, setSelectedId] = useState(null) + const params = useParams() + const router = useRouter() + + useShallowEffect(() => { + detailState.findUnique.load(params?.id as string) + }, []) + + + const handleHapus = () => { + if (selectedId) { + detailState.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey") + } + } + + if (!detailState.findUnique.data) { + return ( + + + + ) + } + + return ( + + + + + + + Detail List Survey + {detailState.findUnique.data ? ( + + + + Judul + {detailState.findUnique.data?.title} + + + Total Responden + {detailState.findUnique.data?.totalRespondents} + + + Rata-rata Skor + {detailState.findUnique.data?.averageScore} + + + + + + + + ) : null} + + + + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text='Apakah anda yakin ingin menghapus list survey ini?' + /> + + ); +} + +export default DetailKegiatanDesa; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/create/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/create/page.tsx new file mode 100644 index 00000000..d229a8df --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/create/page.tsx @@ -0,0 +1,78 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan-masyarakat'; +import colors from '@/con/colors'; +import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; +import { useProxy } from 'valtio/utils'; + + + +function CreateListSurvey() { + const router = useRouter(); + const stateCreate = useProxy(indeksKepuasanState.surveyState) + + + useEffect(() => { + stateCreate.findMany.load(); + }, []); + + const resetForm = () => { + stateCreate.create.form = { + title: "", + totalRespondents: 0, + averageScore: 0, + }; + }; + const handleSubmit = async () => { + await stateCreate.create.create(); + resetForm(); + router.push("/admin/landing-page/indeks-kepuasan-masyarakat/list-survey") + } + return ( + + + + + + + + Create List Survey + { + stateCreate.create.form.title = val.target.value; + }} + label={Judul} + placeholder='Masukkan judul' + /> + { + stateCreate.create.form.totalRespondents = Number(val.target.value); + }} + label={Total Responden} + placeholder='Masukkan tahun' + /> + { + stateCreate.create.form.averageScore = Number(val.target.value); + }} + label={Skor} + placeholder='Masukkan skor' + /> + + + + + + + ); +} + +export default CreateListSurvey; diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/page.tsx new file mode 100644 index 00000000..74dc3448 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/list-survey/page.tsx @@ -0,0 +1,103 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; +import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import indeksKepuasanState from '../../../_state/landing-page/indeks-kepuasan-masyarakat'; +import JudulList from '../../../_com/judulList'; + + + +function ListSurveyLandingPage() { + const [search, setSearch] = useState(''); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListSurvey({ search }: { search: string }) { + const listState = useProxy(indeksKepuasanState.surveyState) + const router = useRouter(); + useEffect(() => { + listState.findMany.load() + }, []) + + const filteredData = (listState.findMany.data || []).filter(item => { + const keyword = search.toLowerCase(); + return ( + item.title.toLowerCase().includes(keyword) || + item.totalRespondents.toString().includes(keyword) || + item.averageScore.toString().includes(keyword) + ); + }); + + if (!listState.findMany.data) { + return ( + + + + ) + } + + return ( + + + + + + + + + Judul + Total Responden + Skor Rata-rata + Detail + + + + {filteredData.map((item) => ( + + + + {item.title} + + + + {item.totalRespondents} + + + {item.averageScore} + + + + + + ))} + +
+
+
+
+
+ ) +} + +export default ListSurveyLandingPage; diff --git a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/page.tsx b/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/page.tsx deleted file mode 100644 index d9274881..00000000 --- a/src/app/admin/(dashboard)/landing-page/indeks-kepuasan-masyarakat/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -function Page() { - return ( -
- Indeks Kepuasan MAsyarakat -
- ); -} - -export default Page; diff --git a/src/app/admin/_com/list_PageAdmin.tsx b/src/app/admin/_com/list_PageAdmin.tsx index 45b3932a..763ff658 100644 --- a/src/app/admin/_com/list_PageAdmin.tsx +++ b/src/app/admin/_com/list_PageAdmin.tsx @@ -17,7 +17,7 @@ export const navBar = [ { id: "Landing_Page_3", name: "Indeks Kepuasan Masyarakat", - path: "/admin/landing-page/indeks-kepuasan-masyarakat" + path: "/admin/landing-page/indeks-kepuasan-masyarakat/list-survey" }, { id: "Landing_Page_4", diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/create.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/create.ts new file mode 100644 index 00000000..037bd590 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/create.ts @@ -0,0 +1,33 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const ageStatCreate = async (context: Context) => { + const body = (await context.body) as { + group: string; + count: number; + monthlyStatId: string; + }; + + try { + const created = await prisma.ageStat.create({ + data: { + group: body.group, + count: body.count, + monthlyStatId: body.monthlyStatId, + }, + }); + + return { + success: true, + message: "Age Stat berhasil dibuat", + data: created, + }; + } catch (error) { + console.error("Gagal create age stat:", error); + return { + success: false, + message: "Gagal membuat data age stat", + }; + } +}; +export default ageStatCreate; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/del.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/del.ts new file mode 100644 index 00000000..8939bc9a --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/del.ts @@ -0,0 +1,24 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const ageStatDelete = async (context: Context) => { + const { id } = context.params as { id: string }; + + try { + await prisma.ageStat.delete({ + where: { id }, + }); + + return { + success: true, + message: "Data age berhasil dihapus", + }; + } catch (error) { + console.error("Gagal hapus Age Stat:", error); + return { + success: false, + message: "Gagal menghapus data age stat", + }; + } +}; +export default ageStatDelete; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/findMany.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/findMany.ts new file mode 100644 index 00000000..ad58a0b4 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/findMany.ts @@ -0,0 +1,26 @@ +import prisma from "@/lib/prisma"; + +export const ageStatFindMany = async () => { + try { + const result = await prisma.ageStat.findMany({ + include: { + MonthlyStat: true, + }, + orderBy: { + id: "desc", + }, + }); + + return { + success: true, + data: result, + }; + } catch (error) { + console.error("Gagal ambil data Age Stat:", error); + return { + success: false, + message: "Gagal mengambil data age stat", + }; + } +}; +export default ageStatFindMany; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/findUnique.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/findUnique.ts new file mode 100644 index 00000000..73c0457d --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/findUnique.ts @@ -0,0 +1,34 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const ageStatFindUnique = async (context: Context) => { + const { id } = context.params as { id: string }; + + try { + const result = await prisma.ageStat.findUnique({ + where: { id }, + include: { + MonthlyStat: true, + }, + }); + + if (!result) { + return { + success: false, + message: "Data age tidak ditemukan", + }; + } + + return { + success: true, + data: result, + }; + } catch (error) { + console.error("Gagal ambil data Age Stat:", error); + return { + success: false, + message: "Terjadi kesalahan saat mengambil data age stat", + }; + } +}; +export default ageStatFindUnique; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/index.ts new file mode 100644 index 00000000..bb6bb145 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/index.ts @@ -0,0 +1,31 @@ +import Elysia, { t } from "elysia"; +import ageStatCreate from "./create"; +import ageStatDelete from "./del"; +import ageStatFindMany from "./findMany"; +import ageStatFindUnique from "./findUnique"; +import ageStatUpdate from "./updt"; + + +const age = new Elysia({ + prefix: "/age", + tags: ["Landing Page/Indeks Kepuasan Masyarakat/Age"], +}) + .post("/create", ageStatCreate, { + body: t.Object({ + monthlyStatId: t.String(), + group: t.String(), + count: t.Number(), + }), + }) + .get("/findMany", ageStatFindMany) + .get("/:id", ageStatFindUnique) + .put("/:id", ageStatUpdate, { + body: t.Object({ + group: t.String(), + count: t.Number(), + monthlyStatId: t.String(), + }), + }) + .delete("/del/:id", ageStatDelete); + +export default age; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/updt.ts new file mode 100644 index 00000000..5beffdf2 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/age/updt.ts @@ -0,0 +1,35 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const ageStatUpdate = async (context: Context) => { + const { id } = context.params as { id: string }; + const body = (await context.body) as { + group?: string; + count?: number; + monthlyStatId?: string; + }; + + try { + const updated = await prisma.ageStat.update({ + where: { id }, + data: { + group: body.group, + count: body.count, + monthlyStatId: body.monthlyStatId, + }, + }); + + return { + success: true, + message: "Data age berhasil diperbarui", + data: updated, + }; + } catch (error) { + console.error("Gagal update Age Stat:", error); + return { + success: false, + message: "Gagal memperbarui data age stat", + }; + } +}; +export default ageStatUpdate; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/create.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/create.ts new file mode 100644 index 00000000..50020cba --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/create.ts @@ -0,0 +1,40 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const genderStatCreate = async (context: Context) => { + const body = (await context.body) as { + laki: number; + perempuan: number; + monthlyStatId: string; + }; + + const total = body.laki + body.perempuan; + const percentLaki = total > 0 ? Number(((body.laki / total) * 100).toFixed(2)) : 0; +const percentPerempuan = total > 0 ? Number(((body.perempuan / total) * 100).toFixed(2)) : 0; + + try { + const created = await prisma.genderStat.create({ + data: { + laki: body.laki, + perempuan: body.perempuan, + total, + percentLaki: Number(percentLaki), + percentPerempuan: Number(percentPerempuan), + monthlyStatId: body.monthlyStatId, + }, + }); + + return { + success: true, + message: "Gender Stat berhasil dibuat", + data: created, + }; + } catch (error) { + console.error("Gagal create gender stat:", error); + return { + success: false, + message: "Gagal membuat data gender stat", + }; + } +}; +export default genderStatCreate; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/del.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/del.ts new file mode 100644 index 00000000..340cff42 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/del.ts @@ -0,0 +1,24 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const genderStatDelete = async (context: Context) => { + const { id } = context.params as { id: string }; + + try { + await prisma.genderStat.delete({ + where: { id }, + }); + + return { + success: true, + message: "Data gender berhasil dihapus", + }; + } catch (error) { + console.error("Gagal hapus Gender Stat:", error); + return { + success: false, + message: "Gagal menghapus data gender", + }; + } +}; +export default genderStatDelete; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/findMany.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/findMany.ts new file mode 100644 index 00000000..eaac659d --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/findMany.ts @@ -0,0 +1,26 @@ +import prisma from "@/lib/prisma"; + +export const genderStatFindMany = async () => { + try { + const result = await prisma.genderStat.findMany({ + include: { + MonthlyStat: true, + }, + orderBy: { + id: "desc", + }, + }); + + return { + success: true, + data: result, + }; + } catch (error) { + console.error("Gagal ambil data Gender Stat:", error); + return { + success: false, + message: "Gagal mengambil data gender stat", + }; + } +}; +export default genderStatFindMany; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/findUnique.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/findUnique.ts new file mode 100644 index 00000000..e09330c4 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/findUnique.ts @@ -0,0 +1,34 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const genderStatFindUnique = async (context: Context) => { + const { id } = context.params as { id: string }; + + try { + const result = await prisma.genderStat.findUnique({ + where: { id }, + include: { + MonthlyStat: true, + }, + }); + + if (!result) { + return { + success: false, + message: "Data gender tidak ditemukan", + }; + } + + return { + success: true, + data: result, + }; + } catch (error) { + console.error("Gagal ambil data Gender Stat:", error); + return { + success: false, + message: "Terjadi kesalahan saat mengambil data gender stat", + }; + } +}; +export default genderStatFindUnique; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/index.ts new file mode 100644 index 00000000..62c9869b --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/index.ts @@ -0,0 +1,37 @@ +import Elysia, { t } from "elysia"; +import genderStatCreate from "./create"; +import genderStatDelete from "./del"; +import genderStatFindMany from "./findMany"; +import genderStatFindUnique from "./findUnique"; +import genderStatUpdate from "./updt"; + + +const gender = new Elysia({ + prefix: "/gender", + tags: ["Landing Page/Indeks Kepuasan Masyarakat/Gender"], +}) + .post("/create", genderStatCreate, { + body: t.Object({ + monthlyStatId: t.String(), + laki: t.Number(), + perempuan: t.Number(), + total: t.Number(), + percentLaki: t.Number(), + percentPerempuan: t.Number(), + }), + }) + .get("/findMany", genderStatFindMany) + .get("/:id", genderStatFindUnique) + .put("/:id", genderStatUpdate, { + body: t.Object({ + laki: t.Number(), + perempuan: t.Number(), + monthlyStatId: t.String(), + total: t.Number(), + percentLaki: t.Number(), + percentPerempuan: t.Number(), + }), + }) + .delete("/del/:id", genderStatDelete); + +export default gender; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/updt.ts new file mode 100644 index 00000000..432a7fda --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/gender/updt.ts @@ -0,0 +1,42 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const genderStatUpdate = async (context: Context) => { + const { id } = context.params as { id: string }; + const body = (await context.body) as { + laki?: number; + perempuan?: number; + monthlyStatId?: string; + }; + + const total = (body.laki ?? 0) + (body.perempuan ?? 0); + const percentLaki = total > 0 ? Number(((body.laki ?? 0) / total) * 100).toFixed(2) : 0; + const percentPerempuan = total > 0 ? Number(((body.perempuan ?? 0) / total) * 100).toFixed(2) : 0; + + try { + const updated = await prisma.genderStat.update({ + where: { id }, + data: { + laki: body.laki, + perempuan: body.perempuan, + total, + percentLaki: Number(percentLaki), + percentPerempuan: Number(percentPerempuan), + monthlyStatId: body.monthlyStatId, + }, + }); + + return { + success: true, + message: "Data gender berhasil diperbarui", + data: updated, + }; + } catch (error) { + console.error("Gagal update Gender Stat:", error); + return { + success: false, + message: "Gagal memperbarui data gender stat", + }; + } +}; +export default genderStatUpdate; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/index.ts new file mode 100644 index 00000000..31b3d436 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/index.ts @@ -0,0 +1,20 @@ +import Elysia from "elysia"; +import gender from "./gender"; +import monthlyStat from "./monthly-stat"; +import survey from "./survey"; +import response from "./response"; +import age from "./age"; + + +const IndeksKepuasanMasyarakat = new Elysia({ + prefix: "/indekskepuasanmasyarakat", + tags: ["Landing Page/Profile/Indeks Kepuasan Masyarakat"], +}) +.use(gender) +.use(monthlyStat) +.use(survey) +.use(response) +.use(age) + + +export default IndeksKepuasanMasyarakat \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/create.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/create.ts new file mode 100644 index 00000000..4d0dd8f1 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/create.ts @@ -0,0 +1,33 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const createMonthlyStat = async (context: Context) => { + const body = await context.body as { + month: string; + respondentsCount: number; + surveyId: string; + }; + + try { + const created = await prisma.monthlyStat.create({ + data: { + month: body.month, + respondentsCount: body.respondentsCount, + surveyId: body.surveyId, + }, + }); + + return { + success: true, + message: "Monthly stat berhasil dibuat", + data: created, + }; + } catch (error) { + console.error("Gagal create monthly stat:", error); + return { + success: false, + message: "Gagal membuat monthly stat", + }; + } +}; +export default createMonthlyStat; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/del.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/del.ts new file mode 100644 index 00000000..273deb56 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/del.ts @@ -0,0 +1,26 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const deleteMonthlyStat = async (context: Context) => { + const { id } = context.params as { id: string }; + + try { + await prisma.monthlyStat.delete({ + where: { id }, + }); + + return { + success: true, + message: "Data statistik bulanan berhasil dihapus", + }; + } catch (error) { + console.error("Gagal hapus MonthlyStat:", error); + return { + success: false, + message: "Gagal menghapus data statistik bulanan", + }; + } +}; + +export default deleteMonthlyStat; + diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/findMany.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/findMany.ts new file mode 100644 index 00000000..78190237 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/findMany.ts @@ -0,0 +1,26 @@ +import prisma from "@/lib/prisma"; + +export const findManyMonthlyStat = async () => { + try { + const result = await prisma.monthlyStat.findMany({ + include: { + survey: true, + }, + orderBy: { + id: "desc", + }, + }); + + return { + success: true, + data: result, + }; + } catch (error) { + console.error("Gagal ambil data MonthlyStat:", error); + return { + success: false, + message: "Gagal mengambil data statistik bulanan", + }; + } +}; +export default findManyMonthlyStat; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/findUnique.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/findUnique.ts new file mode 100644 index 00000000..ef67606e --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/findUnique.ts @@ -0,0 +1,35 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const findUniqueMonthlyStat = async (context: Context) => { + const { id } = context.params as { id: string }; + + try { + const result = await prisma.monthlyStat.findUnique({ + where: { id }, + include: { + survey: true, + }, + }); + + if (!result) { + return { + success: false, + message: "Data tidak ditemukan", + }; + } + + return { + success: true, + data: result, + }; + } catch (error) { + console.error("Gagal ambil data MonthlyStat:", error); + return { + success: false, + message: "Terjadi kesalahan saat mengambil data", + }; + } +}; +export default findUniqueMonthlyStat; + diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/index.ts new file mode 100644 index 00000000..84fe9f11 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/index.ts @@ -0,0 +1,31 @@ +import Elysia, { t } from "elysia"; +import createMonthlyStat from "./create"; +import deleteMonthlyStat from "./del"; +import findManyMonthlyStat from "./findMany"; +import findUniqueMonthlyStat from "./findUnique"; +import updateMonthlyStat from "./updt"; + + +const surveyResponse = new Elysia({ + prefix: "/monthlystat", + tags: ["Landing Page/Indeks Kepuasan Masyarakat/Survey Response"], +}) + .post("/create", createMonthlyStat, { + body: t.Object({ + month: t.String(), + respondentsCount: t.Number(), + surveyId: t.String(), + }), + }) + .get("/findMany", findManyMonthlyStat) + .get("/:id", findUniqueMonthlyStat) + .put("/:id", updateMonthlyStat, { + body: t.Object({ + month: t.String(), + respondentsCount: t.Number(), + surveyId: t.String(), + }), + }) + .delete("/del/:id", deleteMonthlyStat); + +export default surveyResponse; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/updt.ts new file mode 100644 index 00000000..e5feed82 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/monthly-stat/updt.ts @@ -0,0 +1,34 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const updateMonthlyStat = async (context: Context) => { + const { id } = context.params as { id: string }; + const body = await context.body as { + month?: string; + respondentsCount?: number; + }; + + try { + const updated = await prisma.monthlyStat.update({ + where: { id }, + data: { + month: body.month, + respondentsCount: body.respondentsCount, + }, + }); + + return { + success: true, + message: "Data MonthlyStat berhasil diupdate", + data: updated, + }; + } catch (error) { + console.error("Gagal update MonthlyStat:", error); + return { + success: false, + message: "Gagal mengupdate data statistik bulanan", + }; + } +}; + +export default updateMonthlyStat; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/create.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/create.ts new file mode 100644 index 00000000..c53be882 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/create.ts @@ -0,0 +1,33 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const responseStatCreate = async (context: Context) => { + const body = (await context.body) as { + label: string; + count: number; + monthlyStatId: string; + }; + + try { + const created = await prisma.responseStat.create({ + data: { + label: body.label, + count: body.count, + monthlyStatId: body.monthlyStatId, + }, + }); + + return { + success: true, + message: "Response Stat berhasil dibuat", + data: created, + }; + } catch (error) { + console.error("Gagal create response stat:", error); + return { + success: false, + message: "Gagal membuat data response stat", + }; + } +}; +export default responseStatCreate; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/del.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/del.ts new file mode 100644 index 00000000..27e473a7 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/del.ts @@ -0,0 +1,24 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const responseStatDelete = async (context: Context) => { + const { id } = context.params as { id: string }; + + try { + await prisma.responseStat.delete({ + where: { id }, + }); + + return { + success: true, + message: "Data response berhasil dihapus", + }; + } catch (error) { + console.error("Gagal hapus Response Stat:", error); + return { + success: false, + message: "Gagal menghapus data response stat", + }; + } +}; +export default responseStatDelete; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/findMany.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/findMany.ts new file mode 100644 index 00000000..d0f24db0 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/findMany.ts @@ -0,0 +1,26 @@ +import prisma from "@/lib/prisma"; + +export const responseStatFindMany = async () => { + try { + const result = await prisma.responseStat.findMany({ + include: { + MonthlyStat: true, + }, + orderBy: { + id: "desc", + }, + }); + + return { + success: true, + data: result, + }; + } catch (error) { + console.error("Gagal ambil data Response Stat:", error); + return { + success: false, + message: "Gagal mengambil data response stat", + }; + } +}; +export default responseStatFindMany; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/findUnique.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/findUnique.ts new file mode 100644 index 00000000..3c6e7324 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/findUnique.ts @@ -0,0 +1,34 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const responseStatFindUnique = async (context: Context) => { + const { id } = context.params as { id: string }; + + try { + const result = await prisma.responseStat.findUnique({ + where: { id }, + include: { + MonthlyStat: true, + }, + }); + + if (!result) { + return { + success: false, + message: "Data response tidak ditemukan", + }; + } + + return { + success: true, + data: result, + }; + } catch (error) { + console.error("Gagal ambil data Response Stat:", error); + return { + success: false, + message: "Terjadi kesalahan saat mengambil data response stat", + }; + } +}; +export default responseStatFindUnique; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/index.ts new file mode 100644 index 00000000..78fcf06c --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/index.ts @@ -0,0 +1,31 @@ +import Elysia, { t } from "elysia"; +import responseStatCreate from "./create"; +import responseStatDelete from "./del"; +import responseStatFindMany from "./findMany"; +import responseStatFindUnique from "./findUnique"; +import responseStatUpdate from "./updt"; + + +const response = new Elysia({ + prefix: "/response", + tags: ["Landing Page/Indeks Kepuasan Masyarakat/Response"], +}) + .post("/create", responseStatCreate, { + body: t.Object({ + monthlyStatId: t.String(), + label: t.String(), + count: t.Number(), + }), + }) + .get("/findMany", responseStatFindMany) + .get("/:id", responseStatFindUnique) + .put("/:id", responseStatUpdate, { + body: t.Object({ + label: t.String(), + count: t.Number(), + monthlyStatId: t.String(), + }), + }) + .delete("/del/:id", responseStatDelete); + +export default response; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/updt.ts new file mode 100644 index 00000000..94740558 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/response/updt.ts @@ -0,0 +1,35 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const responseStatUpdate = async (context: Context) => { + const { id } = context.params as { id: string }; + const body = (await context.body) as { + label?: string; + count?: number; + monthlyStatId?: string; + }; + + try { + const updated = await prisma.responseStat.update({ + where: { id }, + data: { + label: body.label, + count: body.count, + monthlyStatId: body.monthlyStatId, + }, + }); + + return { + success: true, + message: "Data response berhasil diperbarui", + data: updated, + }; + } catch (error) { + console.error("Gagal update Response Stat:", error); + return { + success: false, + message: "Gagal memperbarui data response stat", + }; + } +}; +export default responseStatUpdate; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/create.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/create.ts new file mode 100644 index 00000000..60ac1aa4 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/create.ts @@ -0,0 +1,42 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +const surveyCreate = async (context: Context) => { + const body = await context.body as { + title: string; + totalRespondents: number; + averageScore: number; + }; + + // Validasi + if (!body.title || !body.totalRespondents || body.averageScore === undefined) { + return { + success: false, + message: "Field title, tahun, dan skor wajib diisi", + }; + } + + try { + const created = await prisma.survey.create({ + data: { + title: body.title, + totalRespondents: body.totalRespondents, + averageScore: body.averageScore, + }, + }); + + return { + success: true, + message: "Survey berhasil dibuat", + data: created, + }; + } catch (error) { + console.error("Gagal create survey :", error); + return { + success: false, + message: "Terjadi kesalahan saat membuat survey ", + }; + } +}; + +export default surveyCreate; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/del.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/del.ts new file mode 100644 index 00000000..22bd804a --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/del.ts @@ -0,0 +1,24 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const surveyDelete = async (context: Context) => { + const { id } = context.params as { id: string }; + + try { + await prisma.survey.delete({ + where: { id }, + }); + + return { + success: true, + message: "Survey berhasil dihapus", + }; + } catch (error) { + console.error("Gagal hapus survey:", error); + return { + success: false, + message: "Gagal menghapus survey", + }; + } +}; +export default surveyDelete; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/findMany.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/findMany.ts new file mode 100644 index 00000000..30a4808d --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/findMany.ts @@ -0,0 +1,28 @@ +import prisma from "@/lib/prisma"; + +export const surveyFindMany = async () => { + try { + const surveys = await prisma.survey.findMany({ + include: { + monthlyStats: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + + return { + success: true, + data: surveys, + }; + } catch (error) { + console.error("Gagal ambil data survey:", error); + return { + success: false, + message: "Terjadi kesalahan saat mengambil data survey", + }; + } +}; + +export default surveyFindMany; + diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/findUnique.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/findUnique.ts new file mode 100644 index 00000000..92173298 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/findUnique.ts @@ -0,0 +1,34 @@ +import { Context } from "elysia"; +import prisma from "@/lib/prisma"; + +export const surveyFindUnique = async (context: Context) => { + const { id } = context.params as { id: string }; + + try { + const survey = await prisma.survey.findUnique({ + where: { id }, + include: { + monthlyStats: true, + }, + }); + + if (!survey) { + return { + success: false, + message: "Survey tidak ditemukan", + }; + } + + return { + success: true, + data: survey, + }; + } catch (error) { + console.error("Gagal ambil data survey:", error); + return { + success: false, + message: "Terjadi kesalahan saat mengambil data survey", + }; + } +}; +export default surveyFindUnique; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/index.ts new file mode 100644 index 00000000..12c70c3e --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/index.ts @@ -0,0 +1,30 @@ +import Elysia, { t } from "elysia"; +import surveyCreate from "./create"; +import surveyUpdate from "./updt"; +import surveyFindMany from "./findMany"; +import surveyFindUnique from "./findUnique"; +import surveyDelete from "./del"; + +const survey = new Elysia({ + prefix: "/survey", + tags: ["Landing Page/Indeks Kepuasan Masyarakat/Survey "], +}) + .post("/create", surveyCreate, { + body: t.Object({ + title: t.String(), + totalRespondents: t.Number(), + averageScore: t.Number(), + }), + }) + .get("/findMany", surveyFindMany) + .get("/:id", surveyFindUnique) + .put("/:id", surveyUpdate, { + body: t.Object({ + title: t.String(), + totalRespondents: t.Number(), + averageScore: t.Number(), + }), + }) + .delete("/del/:id", surveyDelete); + +export default survey; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/updt.ts b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/updt.ts new file mode 100644 index 00000000..a74283a7 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/landing_page/indeks-kepuasan-masyarakat/survey/updt.ts @@ -0,0 +1,49 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormUpdateSurvey = { + title?: string; + tahun?: number; + totalRespondents?: number; + averageScore?: number; +}; + +const surveyUpdate = async (context: Context) => { + const id = context.params?.id as string; + const body = await context.body as FormUpdateSurvey; + + if (!id) { + return { + success: false, + message: "ID survey wajib diisi", + }; + } + + try { + const updated = await prisma.survey.update({ + where: { id }, + data: { + title: body.title, + totalRespondents: body.totalRespondents, + averageScore: body.averageScore, + }, + include: { + monthlyStats: true, + }, + }); + + return { + success: true, + message: "Berhasil update survey ", + data: updated, + }; + } catch (error) { + console.error("Gagal update survey :", error); + return { + success: false, + message: "Gagal update survey ", + }; + } +}; + +export default surveyUpdate; diff --git a/src/app/api/[[...slugs]]/_lib/landing_page/index.ts b/src/app/api/[[...slugs]]/_lib/landing_page/index.ts index 7c524fe1..79c02c88 100644 --- a/src/app/api/[[...slugs]]/_lib/landing_page/index.ts +++ b/src/app/api/[[...slugs]]/_lib/landing_page/index.ts @@ -8,6 +8,7 @@ import SDGSDesa from "./sdgs-desa"; import APBDes from "./apbdes"; import PrestasiDesa from "./prestasi-desa"; import KategoriPrestasi from "./prestasi-desa/kategori-prestasi"; +import IndeksKepuasanMasyarakat from "./indeks-kepuasan-masyarakat"; const LandingPage = new Elysia({ prefix: "/api/landingpage", @@ -23,5 +24,6 @@ const LandingPage = new Elysia({ .use(APBDes) .use(PrestasiDesa) .use(KategoriPrestasi) +.use(IndeksKepuasanMasyarakat) export default LandingPage