Compare commits

...

10 Commits

Author SHA1 Message Date
fc08b2e790 UI & API Admin Kesehatan Done 2025-06-20 08:11:31 +08:00
4a5524ce88 API & UI Kesehatan Sudah Sampai Di Penanganan Darurat 2025-06-20 00:08:13 +08:00
899883ca2a Fix UI & API Admin Kesehatan Puskesmas 2025-06-19 15:40:27 +08:00
10ecc13ad7 API All Kesehatan 2025-06-19 14:12:57 +08:00
58f538425c UI & API Admin Menu Kesehatan 2025-06-19 10:24:50 +08:00
d2f53ff69b Keperluan Deploy 2025-06-18 16:37:08 +08:00
40f0294595 Tambahan buat deploy 2025-06-18 16:32:19 +08:00
6ed0246cea API Profile Desa Udah Clear, API Menu desa udah clear
API & UI Profile Desa Clear
2025-06-18 15:32:06 +08:00
af726043bd API Profile Desa aman, tinggal Profil Perbekel
UI Profile Desa aman, tinggal Profil Perbekel
2025-06-18 14:08:02 +08:00
f4888b53ab API Profile Desa Menu Desa
Fix Eror gallery bagian tabs video
Next UI Profile Desa
2025-06-17 17:30:47 +08:00
143 changed files with 8956 additions and 2014 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -23,7 +23,7 @@
"@mantine/charts": "^7.17.1",
"@mantine/core": "^7.17.4",
"@mantine/dates": "^8.1.0",
"@mantine/dropzone": "^7.17.0",
"@mantine/dropzone": "^8.1.1",
"@mantine/form": "^8.1.0",
"@mantine/hooks": "^7.17.4",
"@mantine/tiptap": "^7.17.4",

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Lambang Desa",
"deskripsi" : "<ul><li>Memperkokoh kerukunan hidup masyarakat dalam jalinan adat, budaya, olahraga, dan agama.</li><li>Meningkatkan kualitas pelayanan publik dengan menerapkan teknologi informasi dan komunikasi terintegrasi.</li><li>Meningkatkan tata kelola pemerintah desa dengan menerapkan prinsip good governance dan good clean government.</li><li>Meningkatkan kualitas pendidikan, kesehatan, Keluarga Berencana serta pengelolaan kependudukan.</li><li>Memperkuat usaha mikro kecil dan menengah (UMKM) dan BUMDesa sebagai pilar ekonomi masyarakat.</li><li>Mewujudkan tatanan kehidupan bermasyarakat yang menjunjung tinggi penegakan hukum dan HAM.</li><li>Meningkatkan perlindungan dan pengelolaan terhadap sumber daya alam dan lingkungan hidup.</li><li>Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa.</li><li>Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata.</li><li>Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa.</li><li>Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata.</li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Maskot Desa",
"deskripsi" : "<p>Pudak adalah bunga dari tanaman sejenis pandan (Pandanaceae). Bentuk bunga ini tersusun dalam beberapa lapisan, terbungkus oleh kelopak warna putih (semacam daun lonjong) yang ujungnya meruncing.</p><p>Bunga Pudak berwarna kuning dan akan terlihat jika kelopak atau pelepahnya telah mekar. Kekhasan dari bunga pudak, yaitu mempunyai aroma wangi yang semerbak nan lembut (tidak menyengat), dan dapat menebar keharuman sepanjang pagi atau pun sore hari. Tanaman ini dapat tumbuh di sepanjang pantai, aliran sungai, di atas batu-batu karang, dan juga di tanah ladang.</p><p>Dalam Kamus Jawa Kuna- Indonesia kata “Pudak” berarti bunga pandan atau Pandanus Moschatus (Mardiwarsito: 1981: 442). Selain itu bunga pudak juga dapat disebut ketaka atau ketaki (Mardiwarsito, 1981: 276). Sedangkan kata “Sategal” berasal dari kata dasar “Tegal” yang berarti ladang (Mardiwarsito, 1981: 593). Jadi Pudak Sategal dapat diartikan sebagai satu ladang luas yang dipenuhi bunga pudak dan menabar keharuman.</p><p>Pada sebuah kesempatan, Ida Pedanda Putu Pemaron menjelaskan mengenai makna dari istilah Pudak Sategal dengan sebuah analogi bahwa, sekuntum bunga pudak memiliki aroma wangi atau keharuman yang sangat kuat, apalagi jika satu ladang penuh bunga pudak, maka dapat dipastikan aroma keharumannya akan membumbung menyebar ke segala penjuru (Wawancara, 18 Mei 2019 di Geria Putra Mandara Kenderan, Tegallalang). “Pudak” ialah sebuah bunga yang memiliki aroma wangi atau keharuman yang semerbak, lembut, dan khas.</p><p>Garapan Tari Maskot Desa Darmasaba Sekar Pudak diwujudkan ke dalam bentuk tari kreasi yang ditarikan secara berkelompok dengan jumlah lima orang penari perempuan (putri).</p><p>Pemilihan penari perempuan dimaksudkan untuk mempresentasikan keindahan, keluwesan, dan keharuman dari bunga pudak. Sedangkan penetapan jumlah penari lima orang didasarkan atas pertimbangan kebutuhan koreografi agar dapat membentuk desain-desain komposisi lantai yang menarik dan dinamis, baik ketika ditarikan di area panggung yang luas atau pun area panggung yang kecil. Penyajian tari maskot ini dirancang dengan durasi waktu 9 menit.</p>"
}
]

View File

@@ -1,6 +1,6 @@
[
{
"id": "1",
"id": "edit",
"biodata": "<p>I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.</p>",
"pengalaman": "<ul><li>2021 - 2027: Perbekel Desa Darmasaba</li><li>2015 - Sekarang: Founder & Managing Director Mantra Legal Consultants & Advocates</li><li>2020 - Sekarang: Founder Ugawa Record Music Studio</li><li>2010 - 2016: Dosen Fakultas Hukum Universitas Mahasaraswati Denpasar</li></ul>",
"pengalamanOrganisasi": "<ul> <li>1996 1997: Ketua OSIS SMP Negeri 1 Abiansemal</li><li>1999 2000: Ketua OSIS SMA Negeri 1 Mengwi</li> <li>2008 2009: Ketua BEM Universitas Mahasaraswati Denpasar</li> <li>2008 2010: Ketua Sekaa Taruna Sila Dharma, Banjar Tengah, Desa Adat Tegal, Darmasaba</li> <li>2020 Sekarang: Pengurus Young Lawyer Committee Peradi Denpasar</li> <li>2021 Sekarang: Dewan Kehormatan Himpunan Pengusaha Muda Indonesia (HIPMI) Badung</li> <li>2023 2028: Komite Tetap Advokasi Bidang Hukum dan Regulasi Kamar Dagang dan Industri Badung</li> </ul>",

View File

@@ -1,11 +0,0 @@
[
{
"id": "1",
"sejarah" : "<p>Asal usul nama Darmasaba tertuang dalam lontar Usada Bali. Seperti di tulis dalam monografi Desa Darmasaba tahun 1980 silam, nama Darmasaba berkaitan dengan keturunan Danghyang Nirarta diceritakan, Sang kawi-wiku asal Daha (Jawa Timur) itu memiliki cucu bernama Ida Pedanda Sakti Manuaba yang tigggal di Desa Kendran Tegalalang Gianyar. Merasa tidak disenangi sang ayah, Ida Pedanda Sakti Manuaba pergi mengembara bersama dua orang pengiringnya. Pengembaraan sang pendeta sampai di pura Sarin Buana di Jimbaran. Saat mengadakan semedi di tempat ini sang pendeta melihat sinar api. Yang sangat jauh di utara. Timbul keinginan Ida Pedanda Manuaba untuk mengunjungi tempat itu. Sampailah sang Pedanda di pura Batan Bila Peguyangan. Disini Ida Pedanda Manuaba singgah menghadap Ida Pedanda Budha yang tinggal disana. Selanjutnya, kedua pendeta bersama-sama menuju arah utara dan singgah di Taman Cang Ana, sebuah taman milik Arya Lanang Blusung. Di tempat ini kedua pendeta bersama-sama melaksanakan semedi dan menetap untuk sementara waktu.</p>",
"visi" : "<p>Mewujudkan Desa Darmasaba yang sejahtera, unggul, religius, berbudaya, dan aman dengan berlandaskan Tri Hita Karana</p>",
"misi" : "<ul><li>Memperkokoh kerukunan hidup masyarakat dalam jalinan adat, budaya, olahraga, dan agama.</li><li>Meningkatkan kualitas pelayanan publik dengan menerapkan teknologi informasi dan komunikasi terintegrasi.</li><li>Meningkatkan tata kelola pemerintah desa dengan menerapkan prinsip good governance dan good clean government.</li><li>Meningkatkan kualitas pendidikan, kesehatan, Keluarga Berencana serta pengelolaan kependudukan.</li><li>Memperkuat usaha mikro kecil dan menengah (UMKM) dan BUMDesa sebagai pilar ekonomi masyarakat.</li><li>Mewujudkan tatanan kehidupan bermasyarakat yang menjunjung tinggi penegakan hukum dan HAM.</li><li>Meningkatkan perlindungan dan pengelolaan terhadap sumber daya alam dan lingkungan hidup.</li><li>Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa.</li><li>Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata.</li></ul>",
"lambang" : "<ul><li>Memperkokoh kerukunan hidup masyarakat dalam jalinan adat, budaya, olahraga, dan agama.</li><li>Meningkatkan kualitas pelayanan publik dengan menerapkan teknologi informasi dan komunikasi terintegrasi.</li><li>Meningkatkan tata kelola pemerintah desa dengan menerapkan prinsip good governance dan good clean government.</li><li>Meningkatkan kualitas pendidikan, kesehatan, Keluarga Berencana serta pengelolaan kependudukan.</li><li>Memperkuat usaha mikro kecil dan menengah (UMKM) dan BUMDesa sebagai pilar ekonomi masyarakat.</li><li>Mewujudkan tatanan kehidupan bermasyarakat yang menjunjung tinggi penegakan hukum dan HAM.</li><li>Meningkatkan perlindungan dan pengelolaan terhadap sumber daya alam dan lingkungan hidup.</li><li>Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa.</li><li>Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata.</li><li>Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa.</li><li>Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata.</li></ul>",
"maskot" : "<p>Pudak adalah bunga dari tanaman sejenis pandan (Pandanaceae). Bentuk bunga ini tersusun dalam beberapa lapisan, terbungkus oleh kelopak warna putih (semacam daun lonjong) yang ujungnya meruncing.</p><p>Bunga Pudak berwarna kuning dan akan terlihat jika kelopak atau pelepahnya telah mekar. Kekhasan dari bunga pudak, yaitu mempunyai aroma wangi yang semerbak nan lembut (tidak menyengat), dan dapat menebar keharuman sepanjang pagi atau pun sore hari. Tanaman ini dapat tumbuh di sepanjang pantai, aliran sungai, di atas batu-batu karang, dan juga di tanah ladang.</p><p>Dalam Kamus Jawa Kuna- Indonesia kata “Pudak” berarti bunga pandan atau Pandanus Moschatus (Mardiwarsito: 1981: 442). Selain itu bunga pudak juga dapat disebut ketaka atau ketaki (Mardiwarsito, 1981: 276). Sedangkan kata “Sategal” berasal dari kata dasar “Tegal” yang berarti ladang (Mardiwarsito, 1981: 593). Jadi Pudak Sategal dapat diartikan sebagai satu ladang luas yang dipenuhi bunga pudak dan menabar keharuman.</p><p>Pada sebuah kesempatan, Ida Pedanda Putu Pemaron menjelaskan mengenai makna dari istilah Pudak Sategal dengan sebuah analogi bahwa, sekuntum bunga pudak memiliki aroma wangi atau keharuman yang sangat kuat, apalagi jika satu ladang penuh bunga pudak, maka dapat dipastikan aroma keharumannya akan membumbung menyebar ke segala penjuru (Wawancara, 18 Mei 2019 di Geria Putra Mandara Kenderan, Tegallalang). “Pudak” ialah sebuah bunga yang memiliki aroma wangi atau keharuman yang semerbak, lembut, dan khas.</p><p>Garapan Tari Maskot Desa Darmasaba Sekar Pudak diwujudkan ke dalam bentuk tari kreasi yang ditarikan secara berkelompok dengan jumlah lima orang penari perempuan (putri).</p><p>Pemilihan penari perempuan dimaksudkan untuk mempresentasikan keindahan, keluwesan, dan keharuman dari bunga pudak. Sedangkan penetapan jumlah penari lima orang didasarkan atas pertimbangan kebutuhan koreografi agar dapat membentuk desain-desain komposisi lantai yang menarik dan dinamis, baik ketika ditarikan di area panggung yang luas atau pun area panggung yang kecil. Penyajian tari maskot ini dirancang dengan durasi waktu 9 menit.</p>",
"profilPerbekelId" : "1"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Sejarah Desa",
"deskripsi": "<p>Asal usul nama Darmasaba tertuang dalam lontar Usada Bali. Seperti di tulis dalam monografi Desa Darmasaba tahun 1980 silam, nama Darmasaba berkaitan dengan keturunan Danghyang Nirarta diceritakan, Sang kawi-wiku asal Daha (Jawa Timur) itu memiliki cucu bernama Ida Pedanda Sakti Manuaba yang tigggal di Desa Kendran Tegalalang Gianyar. Merasa tidak disenangi sang ayah, Ida Pedanda Sakti Manuaba pergi mengembara bersama dua orang pengiringnya. Pengembaraan sang pendeta sampai di pura Sarin Buana di Jimbaran. Saat mengadakan semedi di tempat ini sang pendeta melihat sinar api. Yang sangat jauh di utara. Timbul keinginan Ida Pedanda Manuaba untuk mengunjungi tempat itu. Sampailah sang Pedanda di pura Batan Bila Peguyangan. Disini Ida Pedanda Manuaba singgah menghadap Ida Pedanda Budha yang tinggal disana. Selanjutnya, kedua pendeta bersama-sama menuju arah utara dan singgah di Taman Cang Ana, sebuah taman milik Arya Lanang Blusung. Di tempat ini kedua pendeta bersama-sama melaksanakan semedi dan menetap untuk sementara waktu.</p>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id" : "edit",
"visi" : "<p>Mewujudkan Desa Darmasaba yang sejahtera, unggul, religius, berbudaya, dan aman dengan berlandaskan Tri Hita Karana</p>",
"misi" : "<ul><li>Memperkokoh kerukunan hidup masyarakat dalam jalinan adat, budaya, olahraga, dan agama.</li><li>Meningkatkan kualitas pelayanan publik dengan menerapkan teknologi informasi dan komunikasi terintegrasi.</li><li>Meningkatkan tata kelola pemerintah desa dengan menerapkan prinsip good governance dan good clean government.</li><li>Meningkatkan kualitas pendidikan, kesehatan, Keluarga Berencana serta pengelolaan kependudukan.</li><li>Memperkuat usaha mikro kecil dan menengah (UMKM) dan BUMDesa sebagai pilar ekonomi masyarakat.</li><li>Mewujudkan tatanan kehidupan bermasyarakat yang menjunjung tinggi penegakan hukum dan HAM.</li><li>Meningkatkan perlindungan dan pengelolaan terhadap sumber daya alam dan lingkungan hidup.</li><li>Memperkuat daya saing desa melalui peningkatan mutu sumber daya manusia dan infrastruktur desa berbasis potensi desa.</li><li>Meningkatkan sinergisitas potensi budaya, pertanian dalam arti luas dan pariwisata.</li></ul>"
}
]

View File

@@ -1,6 +1,6 @@
[
{
"id": "1",
"id": "edit",
"name": "I.B Surya Prabhawa Manuaba, S.H., M.H.",
"biodata": "<p>I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar, serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.</p>",
"riwayat": "<ul> <li>2021 - 2027: Perbekel Desa Darmasaba</li> <li>2015 - Sekarang: Founder & Managing Director Mantra Legal Consultants & Advocates</li> <li>2020 - Sekarang: Founder Ugawa Record Music Studio</li> <li>2010 - 2016: Dosen Fakultas Hukum Universitas Mahasaraswati Denpasar</li> </ul>",

View File

@@ -0,0 +1,193 @@
/*
Warnings:
- You are about to drop the column `nomor` on the `DaftarInformasiPublik` table. All the data in the column will be lost.
- You are about to drop the column `image` on the `GalleryFoto` table. All the data in the column will be lost.
- You are about to drop the column `video` on the `GalleryVideo` table. All the data in the column will be lost.
- You are about to drop the column `videosId` on the `GalleryVideo` table. All the data in the column will be lost.
- The primary key for the `IndeksKepuasanMasyarakat` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `profilPerbekelId` on the `ProfileDesa` table. All the data in the column will be lost.
- You are about to drop the column `imageUrl` on the `ProfilePPID` table. All the data in the column will be lost.
- You are about to drop the `Images` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Videos` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `deskripsi` to the `GalleryFoto` table without a default value. This is not possible if the table is not empty.
- Added the required column `deskripsi` to the `GalleryVideo` table without a default value. This is not possible if the table is not empty.
- Added the required column `linkVideo` to the `GalleryVideo` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "GalleryFoto" DROP CONSTRAINT "GalleryFoto_imagesId_fkey";
-- DropForeignKey
ALTER TABLE "GalleryVideo" DROP CONSTRAINT "GalleryVideo_videosId_fkey";
-- DropForeignKey
ALTER TABLE "ProfileDesa" DROP CONSTRAINT "ProfileDesa_profilPerbekelId_fkey";
-- DropIndex
DROP INDEX "GalleryVideo_videosId_key";
-- AlterTable
ALTER TABLE "DaftarInformasiPublik" DROP COLUMN "nomor";
-- AlterTable
ALTER TABLE "GalleryFoto" DROP COLUMN "image",
ADD COLUMN "deskripsi" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "GalleryVideo" DROP COLUMN "video",
DROP COLUMN "videosId",
ADD COLUMN "deskripsi" TEXT NOT NULL,
ADD COLUMN "linkVideo" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "IndeksKepuasanMasyarakat" DROP CONSTRAINT "IndeksKepuasanMasyarakat_pkey",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "IndeksKepuasanMasyarakat_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "IndeksKepuasanMasyarakat_id_seq";
-- AlterTable
ALTER TABLE "ProfileDesa" DROP COLUMN "profilPerbekelId";
-- AlterTable
ALTER TABLE "ProfilePPID" DROP COLUMN "imageUrl",
ADD COLUMN "imageId" TEXT;
-- DropTable
DROP TABLE "Images";
-- DropTable
DROP TABLE "Videos";
-- CreateTable
CREATE TABLE "StrukturPPID" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"imageId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "StrukturPPID_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProfileDesaImage" (
"id" TEXT NOT NULL,
"label" TEXT NOT NULL,
"profileDesaId" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
CONSTRAINT "ProfileDesaImage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PelayananSuratKeterangan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "PelayananSuratKeterangan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PelayananTelunjukSaktiDesa" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"link" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "PelayananTelunjukSaktiDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PelayananPerizinanBerusaha" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"link" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "PelayananPerizinanBerusaha_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PelayananPendudukNonPermanen" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "PelayananPendudukNonPermanen_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Penghargaan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"juara" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "Penghargaan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Posyandu" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nomor" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "Posyandu_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProfilePPID" ADD CONSTRAINT "ProfilePPID_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProfileDesaImage" ADD CONSTRAINT "ProfileDesaImage_profileDesaId_fkey" FOREIGN KEY ("profileDesaId") REFERENCES "ProfileDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProfileDesaImage" ADD CONSTRAINT "ProfileDesaImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GalleryFoto" ADD CONSTRAINT "GalleryFoto_imagesId_fkey" FOREIGN KEY ("imagesId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Penghargaan" ADD CONSTRAINT "Penghargaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Posyandu" ADD CONSTRAINT "Posyandu_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,78 @@
/*
Warnings:
- You are about to drop the column `profileDesaId` on the `ProfileDesaImage` table. All the data in the column will be lost.
- You are about to drop the `ProfileDesa` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `maskotDesaId` to the `ProfileDesaImage` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "ProfileDesaImage" DROP CONSTRAINT "ProfileDesaImage_profileDesaId_fkey";
-- AlterTable
ALTER TABLE "ProfilPerbekel" ADD COLUMN "imageId" TEXT;
-- AlterTable
ALTER TABLE "ProfileDesaImage" DROP COLUMN "profileDesaId",
ADD COLUMN "maskotDesaId" TEXT NOT NULL;
-- DropTable
DROP TABLE "ProfileDesa";
-- CreateTable
CREATE TABLE "SejarahDesa" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "SejarahDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VisiMisiDesa" (
"id" TEXT NOT NULL,
"visi" TEXT NOT NULL,
"misi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "VisiMisiDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LambangDesa" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "LambangDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MaskotDesa" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "MaskotDesa_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "ProfileDesaImage" ADD CONSTRAINT "ProfileDesaImage_maskotDesaId_fkey" FOREIGN KEY ("maskotDesaId") REFERENCES "MaskotDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProfilPerbekel" ADD CONSTRAINT "ProfilPerbekel_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -50,27 +50,32 @@ model AppMenuChild {
// ========================================= FILE STORAGE ========================================= //
model FileStorage {
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
ProfilePPID ProfilePPID[]
StrukturPPID StrukturPPID[]
GalleryFoto GalleryFoto[]
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
link String
Berita Berita[]
PotensiDesa PotensiDesa[]
Posyandu Posyandu[]
StrukturPPID StrukturPPID[]
GalleryFoto GalleryFoto[]
PelayananSuratKeterangan PelayananSuratKeterangan[]
Penghargaan Penghargaan[]
ProfileDesaImage ProfileDesaImage[]
ProfilePPID ProfilePPID[]
ProfilPerbekel ProfilPerbekel[]
Puskesmas Puskesmas[]
ProgramKesehatan ProgramKesehatan[]
PenangananDarurat PenangananDarurat[]
KontakDarurat KontakDarurat[]
Penghargaan Penghargaan[]
InfoWabahPenyakit InfoWabahPenyakit[]
}
//========================================= MENU PPID ========================================= //
@@ -117,6 +122,7 @@ model ProfilePPID {
riwayat String @db.Text
pengalaman String @db.Text
unggulan String @db.Text
imageUrl String?
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
@@ -248,32 +254,68 @@ model GrafikBerdasarkanUmur {
// ========================================= MENU DESA ========================================= //
// ========================================= PROFILE DESA ========================================= //
model ProfileDesa {
id String @id @default(cuid())
sejarah String @db.Text
visi String @db.Text
misi String @db.Text
lambang String @db.Text
maskot String @db.Text
ProfilPerbekel ProfilPerbekel? @relation(fields: [profilPerbekelId], references: [id])
profilPerbekelId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
model SejarahDesa {
id String @id @default(cuid())
judul String @db.Text
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model VisiMisiDesa {
id String @id @default(cuid())
visi String @db.Text
misi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model LambangDesa {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model MaskotDesa {
id String @id @default(cuid())
judul String
deskripsi String @db.Text
images ProfileDesaImage[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model ProfileDesaImage {
id String @id @default(cuid())
label String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
MaskotDesa MaskotDesa @relation(fields: [maskotDesaId], references: [id])
maskotDesaId String
}
model ProfilPerbekel {
id String @id @default(cuid())
biodata String @db.Text
pengalaman String @db.Text
pengalamanOrganisasi String @db.Text
programUnggulan String @db.Text
ProfileDesa ProfileDesa[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
biodata String @db.Text
pengalaman String @db.Text
pengalamanOrganisasi String @db.Text
programUnggulan String @db.Text
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= BERITA ========================================= //
@@ -412,16 +454,16 @@ model PelayananPendudukNonPermanen {
// ========================================= PENGHARGAAN ========================================= //
model Penghargaan {
id String @id @default(cuid())
id String @id @default(cuid())
name String
juara String
deskripsi String @db.Text
deskripsi String @db.Text
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= MENU KESEHATAN ========================================= //
@@ -687,3 +729,98 @@ model Posyandu {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PUSKESMAS ========================================= //
model Puskesmas {
id String @id @default(cuid())
name String
alamat String
jam JamOperasional @relation(fields: [jamId], references: [id])
jamId String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
kontak KontakPuskesmas @relation(fields: [kontakId], references: [id])
kontakId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model JamOperasional {
id String @id @default(cuid())
workDays String
weekDays String
holiday String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
Puskesmas Puskesmas[]
}
model KontakPuskesmas {
id String @id @default(cuid())
kontakPuskesmas String
email String
facebook String
kontakUGD String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
Puskesmas Puskesmas[]
}
// ========================================= PROGRAM KESSEHATAN ========================================= //
model ProgramKesehatan {
id String @id @default(cuid())
name String
deskripsiSingkat String
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PENANGANAN DARURAT ========================================= //
model PenangananDarurat {
id String @id @default(cuid())
name String
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= KONTAK DARURAT ========================================= //
model KontakDarurat {
id String @id @default(cuid())
name String
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= INFO WABAH PENYAKIT ========================================= //
model InfoWabahPenyakit {
id String @id @default(cuid())
name String
deskripsiSingkat String
deskripsiLengkap String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}

View File

@@ -12,6 +12,11 @@ import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json";
import strukturPPID from "./data/ppid/struktur-ppid/strukturPPID.json";
import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json";
import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json";
import sejarahDesa from "./data/desa/profile/sejarah_desa.json";
import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json";
import lambangDesa from "./data/desa/profile/lambang_desa.json";
import maskotDesa from "./data/desa/profile/maskot_desa.json";
import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
(async () => {
for (const l of layanan) {
@@ -30,6 +35,104 @@ import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNo
console.log("layanan success ...");
for (const l of sejarahDesa) {
await prisma.sejarahDesa.upsert({
where: {
id: l.id,
},
update: {
judul: l.judul,
deskripsi: l.deskripsi,
},
create: {
id: l.id,
judul: l.judul,
deskripsi: l.deskripsi,
},
});
}
console.log("sejarah desa success ...");
for (const l of maskotDesa) {
await prisma.maskotDesa.upsert({
where: {
id: l.id,
},
update: {
judul: l.judul,
deskripsi: l.deskripsi,
},
create: {
id: l.id,
judul: l.judul,
deskripsi: l.deskripsi,
},
});
}
console.log("maskot desa success ...");
for (const l of lambangDesa) {
await prisma.lambangDesa.upsert({
where: {
id: l.id,
},
update: {
judul: l.judul,
deskripsi: l.deskripsi,
},
create: {
id: l.id,
judul: l.judul,
deskripsi: l.deskripsi,
},
});
}
console.log("lambang desa success ...");
for (const c of profilPerbekel) {
await prisma.profilPerbekel.upsert({
where: { id: c.id },
update: {
biodata: c.biodata,
pengalaman: c.pengalaman,
pengalamanOrganisasi: c.pengalamanOrganisasi,
programUnggulan: c.programUnggulan,
// imageId tidak di-update
},
create: {
id: c.id,
biodata: c.biodata,
pengalaman: c.pengalaman,
pengalamanOrganisasi: c.pengalamanOrganisasi,
programUnggulan: c.programUnggulan,
// imageId tidak di-create
},
});
}
console.log("✅ profilePerbekel seeded without imageId (editable later via UI)");
for (const l of visiMisiDesa) {
await prisma.visiMisiDesa.upsert({
where: {
id: l.id,
},
update: {
visi: l.visi,
misi: l.misi,
},
create: {
id: l.id,
visi: l.visi,
misi: l.misi,
},
});
}
console.log("visi misi desa success ...");
for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({
where: {
@@ -67,8 +170,7 @@ import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNo
},
});
}
console.log("pelayanan perizinan berusaha success ...");
console.log("pelayanan penduduk non permanen success ...");
for (const s of strukturPPID) {
await prisma.strukturPPID.upsert({

BIN
public/bungapudak.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
public/darmasaba-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
public/klimakstari.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

BIN
public/pohonpudak.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View File

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

BIN
public/tarisekar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

View File

@@ -6,7 +6,7 @@ import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1).max(50),
deskripsi: z.string().min(1).max(50),
deskripsi: z.string().min(1).max(5000),
kategori: z.string().min(1).max(50),
imageId: z.string().min(1).max(50),
content: z.string().min(1).max(5000),

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,214 @@
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 templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsiSingkat: z.string().min(3, "Deskripsi singkat minimal 3 karakter"),
deskripsiLengkap: z.string().min(3, "Deskripsi lengkap minimal 3 karakter"),
imageId: z.string().nonempty(),
});
const defaultForm = {
name: "",
deskripsiSingkat: "",
deskripsiLengkap: "",
imageId: "",
};
const infoWabahPenyakit = proxy({
findMany: {
data: [] as Prisma.InfoWabahPenyakitGetPayload<{
include: {
image: true;
};
}>[],
async load() {
const res = await ApiFetch.api.kesehatan.infowabahpenyakit[
"find-many"
].get();
if (res.status === 200) {
infoWabahPenyakit.findMany.data = res.data?.data ?? [];
}
},
},
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(infoWabahPenyakit.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
infoWabahPenyakit.create.loading = true;
const res = await ApiFetch.api.kesehatan.infowabahpenyakit[
"create"
].post(infoWabahPenyakit.create.form);
if (res.status === 200) {
infoWabahPenyakit.findMany.load();
return toast.success("Info wabah penyakit berhasil disimpan!");
}
return toast.error("Gagal menyimpan info wabah penyakit");
} catch (error) {
console.log((error as Error).message);
} finally {
infoWabahPenyakit.create.loading = false;
}
},
resetForm() {
infoWabahPenyakit.create.form = { ...defaultForm };
},
},
findUnique: {
data: null as Prisma.InfoWabahPenyakitGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/infowabahpenyakit/${id}`);
if (res.ok) {
const data = await res.json();
infoWabahPenyakit.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch info wabah penyakit:", res.statusText);
infoWabahPenyakit.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching info wabah penyakit:", error);
infoWabahPenyakit.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
infoWabahPenyakit.delete.loading = true;
const response = await fetch(`/api/kesehatan/infowabahpenyakit/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Info wabah penyakit berhasil dihapus");
await infoWabahPenyakit.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus info wabah penyakit");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus info wabah penyakit");
} finally {
infoWabahPenyakit.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/infowabahpenyakit/${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 = {
name: data.name,
deskripsiSingkat: data.deskripsiSingkat,
deskripsiLengkap: data.deskripsiLengkap,
imageId: data.imageId,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching info wabah penyakit:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(infoWabahPenyakit.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
infoWabahPenyakit.edit.loading = true;
const response = await fetch(`/api/kesehatan/infowabahpenyakit/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsiSingkat: this.form.deskripsiSingkat,
deskripsiLengkap: this.form.deskripsiLengkap,
imageId: this.form.imageId,
}),
});
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(result.message || "Info wabah penyakit berhasil diupdate");
await infoWabahPenyakit.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update info wabah penyakit");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate info wabah penyakit");
return false;
} finally {
infoWabahPenyakit.edit.loading = false;
}
},
reset() {
infoWabahPenyakit.edit.id = "";
infoWabahPenyakit.edit.form = { ...defaultForm };
},
},
});
export default infoWabahPenyakit;

View File

@@ -0,0 +1,208 @@
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 templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
})
const defaultForm = {
name: "",
deskripsi: "",
imageId: "",
}
const kontakDarurat = proxy({
findMany: {
data: [] as Prisma.KontakDaruratGetPayload<{
include: {
image: true;
};
}>[],
async load() {
const res = await ApiFetch.api.kesehatan.kontakdarurat[
"find-many"
].get();
if (res.status === 200) {
kontakDarurat.findMany.data = res.data?.data ?? [];
}
},
},
create:{
form: {...defaultForm},
loading: false,
async create() {
const cek = templateForm.safeParse(kontakDarurat.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDarurat.create.loading = true;
const res = await ApiFetch.api.kesehatan.kontakdarurat[
"create"
].post(kontakDarurat.create.form);
if (res.status === 200) {
kontakDarurat.findMany.load();
return toast.success("Kontak Darurat berhasil disimpan!");
}
return toast.error("Gagal menyimpan kontak darurat");
} catch (error) {
console.log((error as Error).message);
} finally {
kontakDarurat.create.loading = false;
}
},
resetForm() {
kontakDarurat.create.form = {...defaultForm};
}
},
findUnique: {
data: null as Prisma.KontakDaruratGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/kontakdarurat/${id}`);
if (res.ok) {
const data = await res.json();
kontakDarurat.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kontakDarurat.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kontakDarurat.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
try {
kontakDarurat.delete.loading = true;
const response = await fetch(`/api/kesehatan/kontakdarurat/del/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kontak darurat berhasil dihapus");
await kontakDarurat.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kontak darurat");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kontak darurat");
} finally {
kontakDarurat.delete.loading = false;
}
}
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/kontakdarurat/${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 = {
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching kontak darurat:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(kontakDarurat.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDarurat.edit.loading = true;
const response = await fetch(`/api/kesehatan/kontakdarurat/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
});
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(result.message || "Kontak darurat berhasil diupdate");
await kontakDarurat.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update kontak darurat");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate kontak darurat");
return false;
} finally {
kontakDarurat.edit.loading = false;
}
},
reset() {
kontakDarurat.edit.id = "";
kontakDarurat.edit.form = { ...defaultForm };
},
},
});
export default kontakDarurat

View File

@@ -0,0 +1,208 @@
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 templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
})
const defaultForm = {
name: "",
deskripsi: "",
imageId: "",
}
const penangananDarurat = proxy({
findMany: {
data: [] as Prisma.PenangananDaruratGetPayload<{
include: {
image: true;
};
}>[],
async load() {
const res = await ApiFetch.api.kesehatan.penanganandarurat[
"find-many"
].get();
if (res.status === 200) {
penangananDarurat.findMany.data = res.data?.data ?? [];
}
},
},
create:{
form: {...defaultForm},
loading: false,
async create() {
const cek = templateForm.safeParse(penangananDarurat.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
penangananDarurat.create.loading = true;
const res = await ApiFetch.api.kesehatan.penanganandarurat[
"create"
].post(penangananDarurat.create.form);
if (res.status === 200) {
penangananDarurat.findMany.load();
return toast.success("Penanganan Darurat berhasil disimpan!");
}
return toast.error("Gagal menyimpan penanganan darurat");
} catch (error) {
console.log((error as Error).message);
} finally {
penangananDarurat.create.loading = false;
}
},
resetForm() {
penangananDarurat.create.form = {...defaultForm};
}
},
findUnique: {
data: null as Prisma.PenangananDaruratGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/penanganandarurat/${id}`);
if (res.ok) {
const data = await res.json();
penangananDarurat.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
penangananDarurat.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
penangananDarurat.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
try {
penangananDarurat.delete.loading = true;
const response = await fetch(`/api/kesehatan/penanganandarurat/del/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Penanganan darurat berhasil dihapus");
await penangananDarurat.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus penanganan darurat");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus penanganan darurat");
} finally {
penangananDarurat.delete.loading = false;
}
}
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/penanganandarurat/${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 = {
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching penanganan darurat:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(penangananDarurat.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
penangananDarurat.edit.loading = true;
const response = await fetch(`/api/kesehatan/penanganandarurat/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
});
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(result.message || "Penanganan darurat berhasil diupdate");
await penangananDarurat.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update penanganan darurat");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate penanganan darurat");
return false;
} finally {
penangananDarurat.edit.loading = false;
}
},
reset() {
penangananDarurat.edit.id = "";
penangananDarurat.edit.form = { ...defaultForm };
},
},
});
export default penangananDarurat

View File

@@ -0,0 +1,217 @@
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 templateForm = z.object({
name: z.string().min(1, { message: "Name is required" }),
nomor: z.string().min(1, { message: "Nomor is required" }),
deskripsi: z.string().min(1, { message: "Deskripsi is required" }),
imageId: z.string().nonempty(),
});
const defaultForm = {
name: "",
nomor: "",
deskripsi: "",
imageId: "",
};
const posyandustate = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(posyandustate.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
posyandustate.create.loading = true;
const res = await ApiFetch.api.kesehatan.posyandu["create"].post(posyandustate.create.form);
if (res.status === 200) {
posyandustate.findMany.load();
return toast.success("Posyandu berhasil disimpan!");
}
return toast.error("Gagal menyimpan posyandu");
} catch (error) {
console.log((error as Error).message);
} finally {
posyandustate.create.loading = false;
}
},
resetForm(){
posyandustate.create.form = { ...defaultForm };
}
},
findMany: {
data: null as
| Prisma.PosyanduGetPayload<{
include: {
image: true;
}
}>[]
| null,
async load() {
const res = await ApiFetch.api.kesehatan.posyandu["find-many"].get();
if (res.status === 200) {
posyandustate.findMany.data = res.data?.data ?? [];
}
}
},
findUnique: {
data: null as
| Prisma.PosyanduGetPayload<{
include: {
image: true;
}
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/posyandu/${id}`);
if (res.ok) {
const data = await res.json();
posyandustate.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch posyandu:", res.statusText);
posyandustate.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching posyandu:", error);
posyandustate.findUnique.data = null;
}
}
},
delete: {
loading: false,
async byId(id: string) {
try {
posyandustate.delete.loading = true;
const response = await fetch(`/api/kesehatan/posyandu/del/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Posyandu berhasil dihapus");
await posyandustate.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus posyandu");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus posyandu");
} finally {
posyandustate.delete.loading = false;
}
}
},
edit: {
id: "",
form: {...defaultForm},
loading: false,
async load(id: string) {
if(!id){
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/posyandu/${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 = {
name: data.name,
nomor: data.nomor,
deskripsi: data.deskripsi,
imageId: data.imageId || "",
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching posyandu:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(posyandustate.edit.form);
if(!cek.success){
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
posyandustate.edit.loading = true;
const response = await fetch(`/api/kesehatan/posyandu/${this.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: this.form.name,
nomor: this.form.nomor,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
});
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(result.message || "Posyandu berhasil diperbarui");
await posyandustate.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching posyandu:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return false;
} finally {
posyandustate.edit.loading = false;
}
},
reset() {
posyandustate.edit.id = "";
posyandustate.edit.form = {...defaultForm};
}
}
})
export default posyandustate;

View File

@@ -0,0 +1,214 @@
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 templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsiSingkat: z.string().min(3, "Deskripsi singkat minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
});
const defaultForm = {
name: "",
deskripsiSingkat: "",
deskripsi: "",
imageId: "",
};
const programKesehatan = proxy({
findMany: {
data: [] as Prisma.ProgramKesehatanGetPayload<{
include: {
image: true;
};
}>[],
async load() {
const res = await ApiFetch.api.kesehatan.programkesehatan[
"find-many"
].get();
if (res.status === 200) {
programKesehatan.findMany.data = res.data?.data ?? [];
}
},
},
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(programKesehatan.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programKesehatan.create.loading = true;
const res = await ApiFetch.api.kesehatan.programkesehatan[
"create"
].post(programKesehatan.create.form);
if (res.status === 200) {
programKesehatan.findMany.load();
return toast.success("Program Kesehatan berhasil disimpan!");
}
return toast.error("Gagal menyimpan program kesehatan");
} catch (error) {
console.log((error as Error).message);
} finally {
programKesehatan.create.loading = false;
}
},
resetForm() {
programKesehatan.create.form = { ...defaultForm };
},
},
findUnique: {
data: null as Prisma.ProgramKesehatanGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/programkesehatan/${id}`);
if (res.ok) {
const data = await res.json();
programKesehatan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch program kesehatan:", res.statusText);
programKesehatan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching program kesehatan:", error);
programKesehatan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
programKesehatan.delete.loading = true;
const response = await fetch(`/api/kesehatan/programkesehatan/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Program kesehatan berhasil dihapus");
await programKesehatan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus program kesehatan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus program kesehatan");
} finally {
programKesehatan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/programkesehatan/${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 = {
name: data.name,
deskripsiSingkat: data.deskripsiSingkat,
deskripsi: data.deskripsi,
imageId: data.imageId,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching program kesehatan:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async update() {
const cek = templateForm.safeParse(programKesehatan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programKesehatan.edit.loading = true;
const response = await fetch(`/api/kesehatan/programkesehatan/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsiSingkat: this.form.deskripsiSingkat,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
});
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(result.message || "Program kesehatan berhasil diupdate");
await programKesehatan.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update program kesehatan");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate program kesehatan");
return false;
} finally {
programKesehatan.edit.loading = false;
}
},
reset() {
programKesehatan.edit.id = "";
programKesehatan.edit.form = { ...defaultForm };
},
},
});
export default programKesehatan;

View File

@@ -0,0 +1,298 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
// Validasi form
const templateForm = z.object({
name: z.string().min(1),
alamat: z.string().min(1),
imageId: z.string().min(1),
jam: z.object({
workDays: z.string().min(1),
weekDays: z.string().min(1),
holiday: z.string().min(1),
}),
kontak: z.object({
kontakPuskesmas: z.string().min(1),
email: z.string().min(1),
facebook: z.string().min(1),
kontakUGD: z.string().min(1),
}),
});
// Default form
const defaultForm = {
name: "",
alamat: "",
imageId: "",
jam: {
workDays: "",
weekDays: "",
holiday: "",
},
kontak: {
kontakPuskesmas: "",
email: "",
facebook: "",
kontakUGD: "",
},
image: undefined,
};
const puskesmasState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async submit() {
const cek = templateForm.safeParse(puskesmasState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => v.path.join(".")).join(", ")}] required`;
return toast.error(err);
}
try {
puskesmasState.create.loading = true;
console.log('Form data:', puskesmasState.create.form);
interface ErrorResponse {
message?: string;
error?: string;
errors?: Record<string, string[]>;
}
const payload = {
name: puskesmasState.create.form.name,
alamat: puskesmasState.create.form.alamat,
imageId: puskesmasState.create.form.imageId,
jam: {
workDays: puskesmasState.create.form.jam.workDays,
weekDays: puskesmasState.create.form.jam.weekDays,
holiday: puskesmasState.create.form.jam.holiday,
},
kontak: {
kontakPuskesmas: puskesmasState.create.form.kontak.kontakPuskesmas,
email: puskesmasState.create.form.kontak.email,
facebook: puskesmasState.create.form.kontak.facebook,
kontakUGD: puskesmasState.create.form.kontak.kontakUGD,
},
};
console.log('Sending payload:', JSON.stringify(payload, null, 2));
try {
const res = await ApiFetch.api.kesehatan.puskesmas.create.post(payload);
console.log('API Response:', res);
if (res.status === 200) {
await puskesmasState.findMany.load();
toast.success("Berhasil menambahkan puskesmas");
return res;
} else {
console.error('API Error Response:', {
status: res.status,
data: res.data
});
const errorData = res.data as ErrorResponse;
let errorMessage = 'Gagal menambahkan puskesmas';
if (errorData?.message) {
errorMessage = errorData.message;
} else if (errorData?.error) {
errorMessage = errorData.error;
} else if (errorData?.errors) {
errorMessage = Object.entries(errorData.errors)
.map(([field, messages]) => `${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`)
.join('; ');
}
console.error('Extracted error message:', errorMessage);
toast.error(errorMessage);
throw new Error(errorMessage);
}
} catch (error) {
console.error('Error in API call:', {
error,
errorString: String(error),
jsonError: error instanceof Error ? {
name: error.name,
message: error.message,
stack: error.stack
} : 'Not an Error instance'
});
throw error;
}
} catch (error) {
console.error("Error in puskesmas submit:", {
error,
errorString: String(error),
errorType: typeof error,
isErrorInstance: error instanceof Error,
errorDetails: error instanceof Error ? {
name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause
} : null
});
let errorMessage = "Terjadi kesalahan saat menambahkan puskesmas";
if (error instanceof Error) {
errorMessage = error.message || errorMessage;
} else if (error && typeof error === 'object' && 'message' in error) {
errorMessage = String((error as { message: unknown }).message);
} else if (typeof error === 'string') {
errorMessage = error;
}
console.error('Displaying error to user:', errorMessage);
toast.error(errorMessage);
throw error;
} finally {
puskesmasState.create.loading = false;
}
},
resetForm() {
puskesmasState.create.form = { ...defaultForm };
}
},
findMany: {
data: null as Prisma.PuskesmasGetPayload<{
include: { image: true; jam: true; kontak: true };
}>[] | null,
async load() {
const res = await ApiFetch.api.kesehatan.puskesmas["find-many"].get();
if (res.status === 200) {
puskesmasState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PuskesmasGetPayload<{
include: { image: true; jam: true; kontak: true };
}> | null,
async load(id: string) {
const res = await fetch(`/api/kesehatan/puskesmas/${id}`);
if (res.ok) {
const data = await res.json();
puskesmasState.findUnique.data = data.data ?? null;
} else {
toast.error("Gagal load data puskesmas");
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
const res = await fetch(`/api/kesehatan/puskesmas/${id}`);
if (!res.ok) {
toast.error("Gagal memuat data");
return;
}
const result = await res.json();
const data = result.data;
puskesmasState.edit.id = data.id;
puskesmasState.edit.form = {
name: data.name,
alamat: data.alamat,
imageId: data.imageId,
jam: {
workDays: data.jam.workDays,
weekDays: data.jam.weekDays,
holiday: data.jam.holiday,
},
kontak: {
kontakPuskesmas: data.kontak.kontakPuskesmas,
email: data.kontak.email,
facebook: data.kontak.facebook,
kontakUGD: data.kontak.kontakUGD,
},
image: data.image,
};
},
async submit() {
const cek = templateForm.safeParse(puskesmasState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => v.path.join(".")).join(", ")}] required`;
toast.error(err);
return false;
}
try {
puskesmasState.edit.loading = true;
const payload = {
name: puskesmasState.edit.form.name,
alamat: puskesmasState.edit.form.alamat,
imageId: puskesmasState.edit.form.imageId,
jam: { ...puskesmasState.edit.form.jam },
kontak: { ...puskesmasState.edit.form.kontak }
};
const res = await fetch(`/api/kesehatan/puskesmas/${puskesmasState.edit.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.message || "Update gagal");
}
toast.success("Berhasil update puskesmas");
await puskesmasState.findMany.load();
return true;
} catch (err) {
toast.error(err instanceof Error ? err.message : "Terjadi kesalahan saat update");
return false;
} finally {
puskesmasState.edit.loading = false;
}
},
reset() {
puskesmasState.edit.id = "";
puskesmasState.edit.form = { ...defaultForm };
}
},
delete: {
loading: false,
async byId(id: string) {
try {
puskesmasState.delete.loading = true;
const res = await fetch(`/api/kesehatan/puskesmas/del/${id}`, {
method: "DELETE",
});
const result = await res.json();
if (res.ok && result.success) {
toast.success("Puskesmas berhasil dihapus");
await puskesmasState.findMany.load();
} else {
toast.error(result.message || "Gagal menghapus");
}
} catch {
toast.error("Terjadi kesalahan saat menghapus");
} finally {
puskesmasState.delete.loading = false;
}
}
}
});
export default puskesmasState;

View File

@@ -5,9 +5,9 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateGrafikJenisKelamin = z.object({
laki: z.string().min(2, "Data laki-laki harus diisi"),
perempuan: z.string().min(2, "Data perempuan harus diisi"),
});
laki: z.string().min(1, "Data laki-laki harus diisi"),
perempuan: z.string().min(1, "Data perempuan harus diisi"),
});
type GrafikJenisKelamin = Prisma.GrafikBerdasarkanJenisKelaminGetPayload<{
select: {

View File

@@ -5,10 +5,10 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateGrafikUmur = z.object({
remaja: z.string().min(2, "Data remaja harus diisi"),
dewasa: z.string().min(2, "Data dewasa harus diisi"),
orangtua: z.string().min(2, "Data orangtua harus diisi"),
lansia: z.string().min(2, "Data lansia harus diisi"),
remaja: z.string().min(1, "Data remaja harus diisi"),
dewasa: z.string().min(1, "Data dewasa harus diisi"),
orangtua: z.string().min(1, "Data orangtua harus diisi"),
lansia: z.string().min(1, "Data lansia harus diisi"),
});
type GrafikUmur = Prisma.GrafikBerdasarkanUmurGetPayload<{

View File

@@ -6,7 +6,7 @@ import { z } from "zod";
const templateGrafikHasilKepuasanMasyarakat = z.object({
label: z.string().min(2, "Label harus diisi"),
kepuasan: z.string().min(2, "Kepuasan harus diisi"),
kepuasan: z.string().min(1, "Kepuasan harus diisi"),
});
type GrafikHasilKepuasanMasyarakat = Prisma.IndeksKepuasanMasyarakatGetPayload<{

View File

@@ -125,16 +125,6 @@ function EditBerita() {
placeholder="masukkan judul"
/>
<SelectCategory
value={formData.kategoriBeritaId}
onChange={(val) => {
setFormData({
...formData,
kategoriBeritaId: val?.id || ''
});
}}
/>
<TextInput
value={formData.deskripsi}
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
@@ -174,6 +164,16 @@ function EditBerita() {
/>
</Box>
<SelectCategory
value={formData.kategoriBeritaId}
onChange={(val) => {
setFormData({
...formData,
kategoriBeritaId: val?.id || ''
});
}}
/>
<Button onClick={handleSubmit}>Edit Berita</Button>
</Stack>
</Paper>

View File

@@ -63,9 +63,9 @@ export default function CreateBerita() {
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
@@ -79,8 +79,9 @@ export default function CreateBerita() {
placeholder="masukkan judul"
/>
<SelectCategory
value={beritaState.berita.create.form.kategoriBeritaId}
onChange={(val) => {
beritaState.berita.create.form.kategoriBeritaId = val.id;
beritaState.berita.create.form.kategoriBeritaId = val?.id || "";
}}
/>
<TextInput
@@ -114,10 +115,10 @@ export default function CreateBerita() {
<Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text>
<CreateEditor
value={beritaState.berita.create.form.content}
onChange={(htmlContent) => {
beritaState.berita.create.form.content = htmlContent;
}}
value={beritaState.berita.create.form.content}
onChange={(htmlContent) => {
beritaState.berita.create.form.content = htmlContent;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
@@ -126,26 +127,37 @@ export default function CreateBerita() {
</Box>
);
function SelectCategory({
onChange,
}: {
interface SelectCategoryProps {
onChange: (value: Prisma.KategoriBeritaGetPayload<{
select: {
name: true;
id: true;
};
}>) => void;
}) {
}> | null) => void;
value?: string | null;
defaultValue?: string | null;
}
function SelectCategory({
onChange,
value,
defaultValue,
}: SelectCategoryProps) {
const categoryState = useProxy(stateDashboardBerita.category);
useShallowEffect(() => {
categoryState.findMany.load();
categoryState.findMany.load().then(() => {
console.log("Kategori berhasil dimuat:", categoryState.findMany.data);
});
}, []);
if (!categoryState.findMany.data) {
return <Skeleton height={38} />;
}
const selectedValue = value || defaultValue;
return (
<Select
label={<Text fz={"sm"} fw={"bold"}>Kategori</Text>}
@@ -154,13 +166,19 @@ export default function CreateBerita() {
label: item.name,
value: item.id,
}))}
onChange={(val) => {
const selected = categoryState.findMany.data?.find((item) => item.id === val);
if (selected) {
onChange(selected);
value={selectedValue || null}
onChange={(val: string | null) => {
if (val) {
const selected = categoryState.findMany.data?.find((item) => item.id === val);
if (selected) {
onChange(selected);
}
} else {
onChange(null);
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
/>
);

View File

@@ -1,18 +1,31 @@
export function convertYoutubeUrlToEmbed(url: string): string | null {
const watchRegex = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/;
const shortRegex = /(?:https?:\/\/)?youtu\.be\/([^?]+)/;
export function convertYoutubeUrlToEmbed(url: string) {
const videoIdMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
return videoIdMatch ? `https://www.youtube.com/embed/${videoIdMatch[1]}` : null;
}
// (url: string): string | null {
// const watchRegex = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/;
// const shortRegex = /(?:https?:\/\/)?youtu\.be\/([^?]+)/;
const matchWatch = url.match(watchRegex);
const matchShort = url.match(shortRegex);
// const matchWatch = url.match(watchRegex);
// const matchShort = url.match(shortRegex);
if (matchWatch) {
return `https://www.youtube.com/embed/${matchWatch[1]}`;
}
// if (matchWatch) {
// return `https://www.youtube.com/embed/${matchWatch[1]}`;
// }
if (matchShort) {
return `https://www.youtube.com/embed/${matchShort[1]}`;
}
// if (matchShort) {
// return `https://www.youtube.com/embed/${matchShort[1]}`;
// }
return null;
}
// return null;
// }

View File

@@ -11,18 +11,16 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
function EditVideo() {
const router = useRouter();
const [embedLink, setEmbedLink] = useState("");
const router = useRouter();
const videoState = useProxy(stateGallery.video)
const params = useParams()
const [formData, setFormData] = useState({
name: videoState.findUnique.data?.name || '',
deskripsi: videoState.findUnique.data?.deskripsi || '',
linkVideo: videoState.findUnique.data?.linkVideo || '',
})
name: '',
deskripsi: '',
linkVideo: '',
});
useEffect(() => {
const loadVideo = async () => {
@@ -36,8 +34,6 @@ function EditVideo() {
deskripsi: data.deskripsi || '',
linkVideo: data.linkVideo || '',
});
const embed = convertYoutubeUrlToEmbed(data.linkVideo);
setEmbedLink(embed || "");
}
} catch (error) {
console.error('Error loading video:', error);
@@ -47,7 +43,15 @@ function EditVideo() {
loadVideo();
}, [params?.id]);
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
if (!converted) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
return;
}
try {
videoState.update.form = {
...videoState.update.form,
@@ -55,11 +59,6 @@ function EditVideo() {
deskripsi: formData.deskripsi,
linkVideo: formData.linkVideo,
};
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
if (!converted) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
return;
}
await videoState.update.update();
toast.success('Video berhasil diperbarui!');
router.push('/admin/desa/gallery/video');
@@ -80,29 +79,23 @@ function EditVideo() {
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Video</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Video</Text>}
placeholder='Masukkan judul video'
value={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value,
})
setFormData({ ...formData, name: val.target.value });
}}
/>
<Box>
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={formData.linkVideo}
onChange={(e) => {
setFormData({
...formData,
linkVideo: e.currentTarget.value,
})
const embed = convertYoutubeUrlToEmbed(e.currentTarget.value);
setEmbedLink(embed || "");
setFormData({ ...formData, linkVideo: e.currentTarget.value });
}}
required
/>
@@ -118,18 +111,17 @@ function EditVideo() {
></iframe>
)}
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val,
})
setFormData({ ...formData, deskripsi: val });
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>

View File

@@ -16,7 +16,7 @@ function CreateVideo() {
const videoState = useProxy(stateGallery.video)
const router = useRouter();
const [link, setLink] = useState("");
const [embedLink, setEmbedLink] = useState("");
const embedLink = convertYoutubeUrlToEmbed(link);
const resetForm = () => {
videoState.create.form = {
@@ -26,15 +26,17 @@ function CreateVideo() {
};
};
const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(videoState.create.form.linkVideo);
if (!converted) {
if (!embedLink) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
return;
}
videoState.create.form.linkVideo = embedLink; // pastikan diset di sini juga (jaga-jaga)
await videoState.create.create();
resetForm();
router.push("/admin/desa/gallery/video")
router.push("/admin/desa/gallery/video");
};
return (
<Box>
@@ -63,8 +65,6 @@ function CreateVideo() {
value={link}
onChange={(e) => {
setLink(e.currentTarget.value);
const embed = convertYoutubeUrlToEmbed(e.currentTarget.value);
setEmbedLink(embed || "");
}}
required
/>

View File

@@ -0,0 +1,62 @@
/* 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 LayoutTabsDetail({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Profile Desa",
value: "profiledesa",
href: "/admin/desa/profile/profile-desa"
},
{
label: "Profile Perbekel",
value: "profileperbekel",
href: "/admin/desa/profile/profile-perbekel"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Profile Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsDetail;

View File

@@ -0,0 +1,71 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabsEdit({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Sejarah Desa",
value: "sejarahdesa",
href: "/admin/desa/profile/edit/sejarah_desa"
},
{
label: "Visi Misi Desa",
value: "visimisidesa",
href: "/admin/desa/profile/edit/visi_misi_desa"
},
{
label: "Lambang Desa",
value: "lambangdesa",
href: "/admin/desa/profile/edit/lambang_desa"
},
{
label: "Maskot Desa",
value: "maskotdesa",
href: "/admin/desa/profile/edit/maskot_desa"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Tabs color={colors['blue-button']} variant='default' value={activeTab} onChange={handleTabChange}>
<TabsList>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabsEdit;

View File

@@ -0,0 +1,11 @@
'use client'
import LayoutTabsDetail from "./_lib/layoutTabsDetail"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabsDetail>
{children}
</LayoutTabsDetail>
)
}

View File

@@ -1,53 +0,0 @@
import colors from '@/con/colors';
import { Stack, Title, Tabs, TabsList, TabsTab, TabsPanel } from '@mantine/core';
import React from 'react';
import SejarahDesa from './ui/sejarah_desa/page';
import VisiMisiDesa from './ui/visi_misi_desa/page';
import LambangDesa from './ui/lambang_desa/page';
import MaskotDesa from './ui/maskot_desa/page';
import ProfilePerbekel from './ui/profile_perbekel/page';
function Page() {
return (
<Stack >
<Title order={3}>Profile Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' defaultValue={"Sejarah Desa"}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="Sejarah Desa">
Sejarah Desa
</TabsTab>
<TabsTab value="Visi Misi Desa">
Visi Misi Desa
</TabsTab>
<TabsTab value="Lambang Desa">
Lambang Desa
</TabsTab>
<TabsTab value="Maskot Desa">
Maskot Desa
</TabsTab>
<TabsTab value="Profile Perbekel">
Profile Perbekel
</TabsTab>
</TabsList>
<TabsPanel value="Sejarah Desa">
<SejarahDesa/>
</TabsPanel>
<TabsPanel value="Visi Misi Desa">
<VisiMisiDesa/>
</TabsPanel>
<TabsPanel value="Lambang Desa">
<LambangDesa/>
</TabsPanel>
<TabsPanel value="Maskot Desa">
<MaskotDesa/>
</TabsPanel>
<TabsPanel value="Profile Perbekel">
<ProfilePerbekel/>
</TabsPanel>
</Tabs>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,126 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function Page() {
const lambangState = useProxy(stateProfileDesa.lambangDesa)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
return;
}
const data = await lambangState.findUnique.load(id);
if (data) {
lambangState.update.initialize(data);
}
};
loadData();
return () => {
lambangState.update.reset();
lambangState.findUnique.reset(); // opsional: reset juga data lama
};
}, [params?.id, router]);
const handleSubmit = async () => {
if (isSubmitting || !lambangState.update.form.judul.trim()) {
toast.error("Judul wajib diisi");
return;
}
setIsSubmitting(true)
try {
const success = await lambangState.update.submit()
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update lambang desa:", error);
toast.error("Terjadi kesalahan saat update lambang desa");
} finally {
setIsSubmitting(false);
}
}
const handleBack = () => {
router.back()
}
if (
lambangState.findUnique.loading ||
!lambangState.findUnique.data ||
lambangState.update.loading
) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
</Center>
</Box>
);
}
return (
<Box>
<Stack gap={'xs'}>
<Group>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Box>
<Box>
<Stack>
<Title order={3}>Edit Lambang Desa</Title>
<TextInput
label={<Text fz={"md"} fw={"bold"}>Judul</Text>}
placeholder="Judul"
value={lambangState.update.form.judul}
onChange={(e) => lambangState.update.form.judul = e.currentTarget.value}
error={!lambangState.update.form.judul && "Judul wajib diisi"}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={lambangState.update.form.deskripsi}
onChange={(val) => lambangState.update.form.deskripsi = val}
/>
</Box>
<Group>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,244 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function Page() {
const maskotState = useProxy(stateProfileDesa.maskotDesa)
const router = useRouter()
const params = useParams()
const [images, setImages] = useState<
Array<{ file: File; preview: string; label: string }>
>([]);
const [formData, setFormData] = useState({
judul: maskotState.update.form.judul || '',
deskripsi: maskotState.update.form.deskripsi || '',
images: [] as Array<{ label: string; imageId: string }>
})
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await maskotState.findUnique.load(id);
if (data) {
// 🔥 INI YANG KURANG!
maskotState.update.initialize(data);
setFormData({
judul: data.judul || '',
deskripsi: data.deskripsi || '',
images: (data.images || []).map((img: any) => ({
label: img.label,
imageId: img.image?.id ?? '',
})),
});
if (data?.images?.length > 0 && data.images[0].image?.link) {
setImages(data.images.map((img: any) => ({
file: null,
preview: img.image.link,
label: img.label,
})));
}
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error("Gagal memuat data berita");
}
};
loadData();
}, [params?.id]);
const handleBack = () => {
router.back()
}
const handleSubmit = async () => {
try {
const uploadedImages = [];
// Upload semua gambar baru
for (const img of images) {
if (!img.file || !(img.file instanceof File)) {
toast.error("File tidak valid untuk di-upload");
continue; // atau return kalau kamu mau hentikan semua
}
const res = await ApiFetch.api.fileStorage.create.post({
file: img.file,
name: img.file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload salah satu gambar");
return;
}
uploadedImages.push({
imageId: uploaded.id,
label: img.label || 'main',
});
}
// Update ke global state
maskotState.update.updateField("judul", formData.judul);
maskotState.update.updateField("deskripsi", formData.deskripsi);
maskotState.update.updateField("images", uploadedImages);
const success = await maskotState.update.submit();
if (success) {
toast.success("Maskot berhasil diperbarui!");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update maskot:", error);
toast.error("Gagal update maskot");
}
};
return (
<Box>
<Stack gap={'xs'}>
<Group>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '100%' }}>
<Stack gap={'xs'}>
<Box>
<Box>
<Stack>
<Title order={3}>Edit Maskot Desa</Title>
<TextInput
w={{ base: '100%', md: '50%' }}
label={<Text fz={"md"} fw={"bold"}>Judul</Text>}
placeholder="Masukkan judul"
value={formData.judul}
onChange={(val) => setFormData({ ...formData, judul: val.currentTarget.value })}
/>
<Box w={{ base: '100%', md: '50%' }}>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box w={{ base: '100%', md: '50%' }}>
<Dropzone
onDrop={(files) => {
const newImages = files.map((file) => ({
file,
preview: URL.createObjectURL(file),
label: '',
}));
setImages((prev) => [...prev, ...newImages]);
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
</Box>
</Box>
<SimpleGrid cols={{ base: 2, md: 4 }} >
{images.map((img, index) => (
<Box key={index} mb="md">
<Paper p="sm" radius="md" withBorder style={{ position: 'relative', maxWidth: 300 }}>
<Stack gap={'xs'}>
<Group>
<Button
size="xs"
color="red"
variant="light"
onClick={() => {
const updated = [...images];
updated.splice(index, 1);
setImages(updated);
}}
>
Hapus
</Button>
</Group>
<Image
src={img.preview}
alt={`Preview ${index}`}
width={280}
height={180}
fit="cover"
radius="sm"
/>
<TextInput
label={`Label Gambar ${index + 1}`}
placeholder="Contoh: Logo, Maskot, Dll"
value={img.label}
onChange={(e) => {
const updated = [...images];
updated[index].label = e.currentTarget.value;
setImages(updated);
}}
/>
</Stack>
</Paper>
</Box>
))}
</SimpleGrid>
<Group>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,126 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function Page() {
const sejarahState = useProxy(stateProfileDesa.sejarahDesa)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
return;
}
const data = await sejarahState.findUnique.load(id);
if (data) {
sejarahState.update.initialize(data);
}
};
loadData();
return () => {
sejarahState.update.reset();
sejarahState.findUnique.reset(); // opsional: reset juga data lama
};
}, [params?.id, router]);
const handleSubmit = async () => {
if (isSubmitting || !sejarahState.update.form.judul.trim()) {
toast.error("Judul wajib diisi");
return;
}
setIsSubmitting(true)
try {
const success = await sejarahState.update.submit()
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update sejarah desa:", error);
toast.error("Terjadi kesalahan saat update sejarah desa");
} finally {
setIsSubmitting(false);
}
}
const handleBack = () => {
router.back()
}
if (
sejarahState.findUnique.loading ||
!sejarahState.findUnique.data ||
sejarahState.update.loading
) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
</Center>
</Box>
);
}
return (
<Box>
<Stack gap={'xs'}>
<Group>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Box>
<Box>
<Stack>
<Title order={3}>Edit Sejarah Desa</Title>
<TextInput
label={<Text fz={"md"} fw={"bold"}>Judul</Text>}
placeholder="Judul"
value={sejarahState.update.form.judul}
onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value}
error={!sejarahState.update.form.judul && "Judul wajib diisi"}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={sejarahState.update.form.deskripsi}
onChange={(val) => sejarahState.update.form.deskripsi = val}
/>
</Box>
<Group>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,124 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function Page() {
const visiMisiState = useProxy(stateProfileDesa.visiMisiDesa)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
return;
}
const data = await visiMisiState.findUnique.load(id);
if (data) {
visiMisiState.update.initialize(data);
}
};
loadData();
return () => {
visiMisiState.update.reset();
visiMisiState.findUnique.reset(); // opsional: reset juga data lama
};
}, [params?.id, router]);
const handleSubmit = async () => {
if (isSubmitting || !visiMisiState.update.form.visi.trim()) {
toast.error("Visi wajib diisi");
return;
}
setIsSubmitting(true)
try {
const success = await visiMisiState.update.submit()
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update sejarah desa:", error);
toast.error("Terjadi kesalahan saat update sejarah desa");
} finally {
setIsSubmitting(false);
}
}
const handleBack = () => {
router.back()
}
if (
visiMisiState.findUnique.loading ||
!visiMisiState.findUnique.data ||
visiMisiState.update.loading
) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
</Center>
</Box>
);
}
return (
<Box>
<Stack gap={'xs'}>
<Group>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Box>
<Box>
<Stack>
<Title order={3}>Edit Visi Misi Desa</Title>
<Text fz={"md"} fw={"bold"}>Visi</Text>
<EditEditor
value={visiMisiState.update.form.visi}
onChange={(val) => visiMisiState.update.form.visi = val}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Misi</Text>
<EditEditor
value={visiMisiState.update.form.misi}
onChange={(val) => visiMisiState.update.form.misi = val}
/>
</Box>
<Group>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -0,0 +1,171 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Card, Center, Grid, GridCol, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
import { useSnapshot } from 'valtio';
import stateProfileDesa from '../../../_state/desa/profile';
import { useEffect } from 'react';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function Page() {
const router = useRouter();
const snap = useSnapshot(stateProfileDesa);
// Panggil load data sekali saat komponen mount
useEffect(() => {
stateProfileDesa.sejarahDesa.findUnique.load("edit");
stateProfileDesa.visiMisiDesa.findUnique.load("edit");
stateProfileDesa.lambangDesa.findUnique.load("edit");
stateProfileDesa.maskotDesa.findUnique.load("edit");
}, []);
const sejarah = snap.sejarahDesa.findUnique.data;
const visiMisi = snap.visiMisiDesa.findUnique.data;
const lambang = snap.lambangDesa.findUnique.data;
const maskot = snap.maskotDesa.findUnique.data;
return (
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"lg"}>
<Title order={2}>Preview Profile Desa</Title>
{/* Sejarah Desa */}
{sejarah && (
<Box>
<Stack gap={'lg'}>
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Sejarah Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/desa/profile/profile-desa/${sejarah.id}/sejarah_desa`)}>
<IconEdit size={20} />
</Button>
</GridCol>
</Grid>
<Box pb={30}>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>{sejarah.judul}</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: sejarah.deskripsi }} />
</Paper>
</Paper>
</Stack>
</Box>
)}
{/* Visi Misi Desa */}
{visiMisi && (
<Box>
<Stack gap={'lg'}>
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Visi Misi Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${visiMisi.id}/visi_misi_desa`)} bg={colors['blue-button']}>
<IconEdit size={20} />
</Button>
</GridCol>
</Grid>
<Box pb={30}>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Visi Misi Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fw={"bold"} fz={{ base: "lg", md: "h2" }}>Visi Desa</Text>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: visiMisi.visi }} />
<Text fw={"bold"} fz={{ base: "lg", md: "h2" }}>Misi Desa</Text>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: visiMisi.misi }} />
</Paper>
</Paper>
</Stack>
</Box>
)}
{/* Lambang Desa */}
{lambang && (
<Box>
<Stack gap={'lg'}>
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Lambang Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${lambang.id}/lambang_desa`)} bg={colors['blue-button']}>
<IconEdit size={20} />
</Button>
</GridCol>
</Grid>
<Box pb={30}>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Lambang Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: lambang.deskripsi }} />
</Paper>
</Paper>
</Stack>
</Box>
)}
{/* Maskot Desa */}
{maskot && (
<Box>
<Stack gap={'lg'}>
<Paper p={"md"} bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Maskot Desa</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push(`/admin/desa/profile/profile-desa/${maskot.id}/maskot_desa`)} bg={colors['blue-button']}>
<IconEdit size={20} />
</Button>
</GridCol>
</Grid>
<Box pb={30}>
<Center>
<Image src={"/pudak-icon.png"} alt="" w={{ base: 200, md: 300 }} />
</Center>
<Text c={colors['blue-button']} ta={"center"} fw={"bold"} fz={"2.5rem"}>Maskot Desa</Text>
</Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: maskot.deskripsi }} />
<Group wrap="wrap" gap="md">
{maskot.images.map((img, index) => (
<Card key={index} p="xs" w={220}>
<Image
src={img.image.link}
alt={img.label}
w={200}
h={200}
fit="cover"
radius="md"
style={{ border: '1px solid #ccc', objectFit: 'cover' }}
/>
<Text ta="center" mt="xs" fw="bold">{img.label}</Text>
</Card>
))}
</Group>
</Paper>
</Paper>
</Stack>
</Box>
)}
</Stack>
</Paper>
);
}
export default Page;

View File

@@ -0,0 +1,193 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 ProfilePerbekel() {
const perbekelState = useProxy(stateProfileDesa.profilPerbekel)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-perbekel");
return;
}
const data = await perbekelState.findUnique.load(id);
if (data) {
perbekelState.edit.initialize(data);
}
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
};
loadData();
return () => {
perbekelState.edit.reset();
perbekelState.findUnique.reset(); // opsional: reset juga data lama
};
}, [params?.id, router]);
const handleFileChange = (newFile: File | null) => {
if (!newFile) {
setFile(null);
return;
}
setFile(newFile);
const reader = new FileReader();
reader.onload = (event) => {
setPreviewImage(event.target?.result as string);
};
reader.readAsDataURL(newFile);
};
const handleSubmit = async () => {
if (isSubmitting || !perbekelState.edit.form.biodata.trim()) {
toast.error("Biodata wajib diisi");
return;
}
setIsSubmitting(true)
try {
if (file) {
const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = uploadResponse.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload gambar");
return;
}
perbekelState.edit.form.imageId = uploaded.id;
}
const success = await perbekelState.edit.submit()
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-perbekel");
}
} catch (error) {
console.error("Error update sejarah desa:", error);
toast.error("Terjadi kesalahan saat update sejarah desa");
} finally {
setIsSubmitting(false);
}
}
const handleBack = () => {
router.back()
}
if (
perbekelState.findUnique.loading ||
!perbekelState.findUnique.data ||
perbekelState.edit.loading
) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
</Center>
</Box>
);
}
return (
<Box py={10}>
<Stack gap={'xs'}>
<Group>
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Group>
<Paper bg={colors['white-1']} p={'xs'} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Box>
<Box>
<Stack>
<Title order={3}>Edit Profil Perbekel</Title>
<Text fz={"md"} fw={"bold"}>Biodata</Text>
<EditEditor
value={perbekelState.edit.form.biodata}
onChange={(val) => perbekelState.edit.form.biodata = val}
/>
{/* File Upload */}
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={handleFileChange}
accept="image/*"
/>
{/* Preview Gambar */}
<Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
{previewImage ? (
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
) : (
<Center w={200} h={200} bg="gray.2">
<Stack align="center" gap="xs">
<IconImageInPicture size={48} color="gray" />
<Text size="sm" c="gray">Tidak ada gambar</Text>
</Stack>
</Center>
)}
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Pengalaman</Text>
<EditEditor
value={perbekelState.edit.form.pengalaman}
onChange={(val) => perbekelState.edit.form.pengalaman = val}
/>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Pengalaman Organisasi</Text>
<EditEditor
value={perbekelState.edit.form.pengalamanOrganisasi}
onChange={(val) => perbekelState.edit.form.pengalamanOrganisasi = val}
/>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Program Unggulan</Text>
<EditEditor
value={perbekelState.edit.form.programUnggulan}
onChange={(val) => perbekelState.edit.form.programUnggulan = val}
/>
</Box>
<Group>
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default ProfilePerbekel;

View File

@@ -0,0 +1,112 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Stack, Text, Title } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useSnapshot } from 'valtio';
import stateProfileDesa from '../../../_state/desa/profile';
function Page() {
const router = useRouter();
const snap = useSnapshot(stateProfileDesa);
// Panggil load data sekali saat komponen mount
useEffect(() => {
stateProfileDesa.profilPerbekel.findUnique.load("edit");
}, []);
const perbekel = snap.profilPerbekel.findUnique.data;
return (
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3}>Preview Profile PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push(`/admin/desa/profile/profile-perbekel/${snap.profilPerbekel.findUnique.data?.id}`)}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
{perbekel && (
<Box >
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: "md", md: 100 }}>
<Grid>
<GridCol span={{ base: 12, md: 12 }}>
<Center>
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
</Center>
</GridCol>
<GridCol span={{ base: 12, md: 12 }}>
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.8rem" }} fw={'bold'}>PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA </Text>
</GridCol>
</Grid>
</Box>
<Divider my={"md"} color={colors['blue-button']} />
{/* biodata perbekel */}
<Box px={{ base: 0, md: 50 }} pb={30}>
<Box pb={20} px={{ base: 0, md: 50 }}>
<Paper bg={colors['BG-trans']} w={{ base: "100%", md: "100%" }}>
<Stack gap={0}>
<Center>
<Image
pt={{ base: 0, md: 90 }}
src={perbekel.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }}
alt='Foto Profil PPID'
onError={(e) => {
e.currentTarget.src = "/perbekel.png";
}}
/>
</Center>
<Paper
bg={colors['blue-button']}
py={20}
className="glass3"
px={{ base: 10, md: 10 }}
>
<Text ta={"center"} c={colors['white-1']} fw={"bolder"} fz={{ base: "1.2rem", md: "1.6rem" }}>
I.B. Surya Prabhawa Manuaba, S.H.,M.H.,NL.P.
</Text>
</Paper>
</Stack>
</Paper>
</Box>
<Box pt={10}>
<Box>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Biodata</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.biodata }} />
</Box>
<Box>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman</Text>
<Text fz={{ base: "1rem", md: "1.5rem" }} dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }} />
</Box>
</Box>
<Box pb={30}>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Pengalaman Organisasi</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }} />
</Box>
</Box>
<Box pb={20}>
<Text fz={{ base: "1.125rem", md: "1.6rem" }} fw={'bold'}>Program Kerja Unggulan</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.5rem" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }} />
</Box>
</Box>
</Box>
</Paper>
</Box>
)}
</Stack>
</Paper>
);
}
export default Page;

View File

@@ -1,39 +0,0 @@
import colors from '@/con/colors';
import { Box, SimpleGrid, Paper, Stack, Title, Group, Button, Text } from '@mantine/core';
import React from 'react';
import { DesaEditor } from '../../../_com/desaEditor';
function LambangDesa() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Lambang Desa</Title>
<Text fw={"bold"}>Deskripsi Lambang Desa</Text>
<DesaEditor showSubmit={false} />
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Lambang Desa</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default LambangDesa;

View File

@@ -1,39 +0,0 @@
import colors from '@/con/colors';
import { Box, SimpleGrid, Paper, Stack, Title, Group, Button, Text } from '@mantine/core';
import React from 'react';
import { DesaEditor } from '../../../_com/desaEditor';
function MaskotDesa() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Maskot Desa</Title>
<Text fw={"bold"}>Deskripsi Maskot Desa</Text>
<DesaEditor showSubmit={false} />
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Maskot Desa</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default MaskotDesa;

View File

@@ -1,49 +0,0 @@
import colors from '@/con/colors';
import { Box, SimpleGrid, Paper, Stack, Title, Group, Button, TextInput, Text } from '@mantine/core';
import React from 'react';
import { DesaEditor } from '../../../_com/desaEditor';
function ProfilePerbekel() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Profil Perbekel</Title>
<TextInput
label="Nama Perbekel"
placeholder="masukkan nama perbekel"
/>
<Text fz={"sm"} fw={"bold"}>Biodata</Text>
<DesaEditor showSubmit={false} />
<Text fz={"sm"} fw={"bold"}>Pengalaman</Text>
<DesaEditor showSubmit={false} />
<Text fz={"sm"} fw={"bold"}>Pengalaman Organisasi</Text>
<DesaEditor showSubmit={false} />
<Text fz={"sm"} fw={"bold"}>Program Unggulan</Text>
<DesaEditor showSubmit={false} />
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Profil Perbekel</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default ProfilePerbekel;

View File

@@ -1,17 +0,0 @@
import colors from '@/con/colors';
import { Box, Paper, Stack, Title } from '@mantine/core';
import React from 'react';
function ListPage() {
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Sejarah Desa</Title>
</Stack>
</Paper>
</Box>
);
}
export default ListPage;

View File

@@ -1,65 +0,0 @@
'use client'
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { useProxy } from 'valtio/utils';
import DesaEditorText from '../../../_com/desaEditorText';
import ListPage from './listPage';
import { useShallowEffect } from '@mantine/hooks';
function SejarahDesa() {
const stateSejarah = useProxy(stateProfileDesa.Sejarah)
useShallowEffect(() => {
if (!stateSejarah.findById.data) {
stateSejarah.findById.initialize()
}
}, [])
const submit = () => {
if (stateSejarah.findById.data?.id && stateSejarah.findById.data.sejarah) {
stateSejarah.update.save({
id: stateSejarah.findById.data.id,
sejarah: stateSejarah.findById.data.sejarah
})
}
}
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Sejarah Desa</Title>
<Text fw={"bold"}>Deskripsi Sejarah Desa</Text>
<DesaEditorText
key={stateSejarah.findById.data?.id ?? 'new'}
showSubmit={false}
onChange={(val) => {
if (stateSejarah.findById.data) {
stateSejarah.findById.data.sejarah = val
}
}}
initialContent={stateSejarah.findById.data?.sejarah ?? ""}
/>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
onClick={submit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<ListPage />
</SimpleGrid>
</Box>
);
}
export default SejarahDesa;

View File

@@ -1,64 +0,0 @@
import colors from '@/con/colors';
import { Box, Button, Group, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { DesaEditor } from '../../../_com/desaEditor';
function VisiMisiDesa() {
return (
<Box py={10}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Visi Desa</Title>
<Text fw={"bold"}>Deskripsi Visi Desa</Text>
<DesaEditor showSubmit={false} />
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Visi Desa</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
<SimpleGrid py={30} cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>Misi Desa</Title>
<Text fw={"bold"}>Deskripsi Misi Desa</Text>
<DesaEditor showSubmit={false}/>
<Group>
<Button
mt={10}
bg={colors['blue-button']}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title order={3}>List Misi Desa</Title>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Box>
);
}
export default VisiMisiDesa;

View File

@@ -0,0 +1,150 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 EditInfoWabahPenyakit() {
const infoWabahPenyakitState = useProxy(infoWabahPenyakit)
const router = useRouter();
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: infoWabahPenyakitState.edit.form.name || '',
deskripsiSingkat: infoWabahPenyakitState.edit.form.deskripsiSingkat || '',
deskripsi: infoWabahPenyakitState.edit.form.deskripsiLengkap || '',
imageId: infoWabahPenyakitState.edit.form.imageId || '',
})
useEffect(() => {
const loadInfoWabahPenyakit = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await infoWabahPenyakitState.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsiSingkat: data.deskripsiSingkat || '',
deskripsi: data.deskripsiLengkap || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading program kesehatan:", error);
toast.error("Gagal memuat data program kesehatan");
}
};
loadInfoWabahPenyakit();
}, [params?.id]);
const handleSubmit = async () => {
try {
infoWabahPenyakitState.edit.form = {
...infoWabahPenyakitState.edit.form,
name: formData.name,
deskripsiSingkat: formData.deskripsiSingkat,
deskripsiLengkap: formData.deskripsi,
imageId: formData.imageId,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
infoWabahPenyakitState.edit.form.imageId = uploaded.id;
}
await infoWabahPenyakitState.edit.update();
toast.success("Info wabah penyakit berhasil diperbarui!");
router.push("/admin/kesehatan/info-wabah-penyakit");
} catch (error) {
console.error("Error updating info wabah penyakit:", error);
toast.error("Gagal memuat data info wabah penyakit");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Info Wabah Penyakit</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
value={formData.deskripsiSingkat}
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack>
</Box >
);
}
export default EditInfoWabahPenyakit;

View File

@@ -0,0 +1,103 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailInfoWabahPenyakit() {
const infoWabahPenyakitState = useProxy(infoWabahPenyakit)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const params = useParams()
useShallowEffect(() => {
infoWabahPenyakitState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
infoWabahPenyakitState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/info-wabah-penyakit")
}
}
if (!infoWabahPenyakitState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={400} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Info Wabah Penyakit</Text>
{infoWabahPenyakitState.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>{infoWabahPenyakitState.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>{infoWabahPenyakitState.findUnique.data.deskripsiSingkat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: infoWabahPenyakitState.findUnique.data.deskripsiLengkap }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={infoWabahPenyakitState.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (infoWabahPenyakitState.findUnique.data) {
setSelectedId(infoWabahPenyakitState.findUnique.data.id)
setModalHapus(true)
}
}}
disabled={infoWabahPenyakitState.delete.loading || !infoWabahPenyakitState.findUnique.data}
>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${infoWabahPenyakitState.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus info wabah penyakit ini?"
/>
</Box>
);
}
export default DetailInfoWabahPenyakit;

View File

@@ -1,43 +1,101 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
function CreateInfoWabahPenyakit() {
const router = useRouter();
const infoWabahPenyakitState = useProxy(infoWabahPenyakit)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
// Reset state di valtio
infoWabahPenyakitState.create.form = {
name: "",
deskripsiSingkat: "",
deskripsiLengkap: "",
imageId: "",
};
// Reset state lokal
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Simpan ID gambar ke form
infoWabahPenyakitState.create.form.imageId = uploaded.id;
// Submit data berita
await infoWabahPenyakitState.create.create();
// Reset form setelah submit
resetForm();
router.push("/admin/kesehatan/info-wabah-penyakit")
};
return (
<Box>
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Info Wabah Penyakit</Title>
<TextInput
value={infoWabahPenyakitState.create.form.name}
onChange={(val) => {
infoWabahPenyakitState.create.form.name = val.target.value;
}}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
value={infoWabahPenyakitState.create.form.deskripsiSingkat}
onChange={(val) => {
infoWabahPenyakitState.create.form.deskripsiSingkat = val.target.value;
}}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder="masukkan kategori"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<CreateEditor
value={infoWabahPenyakitState.create.form.deskripsiLengkap}
onChange={(val) => {
infoWabahPenyakitState.create.form.deskripsiLengkap = val;
}}
/>
</Box>
{/* <FileInput
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
@@ -48,25 +106,18 @@ function CreateInfoWabahPenyakit() {
);
setPreviewImage(base64);
}}
/> */}
/>
{/* {previewImage ? (
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)} */}
)}
<Box>
<Text fz="sm" fw="bold">Konten</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Button bg={colors['blue-button']}>
Simpan Potensi
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>

View File

@@ -1,70 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailInfoWabahPenyakit() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Info Wabah/Penyakit</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Info Wabah/Penyakit</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Info Wabah/Penyakit</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>Test Deskripsi</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/kesehatan/info-wabah-penyakit/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus penanganan darurat ini?"
/> */}
</Box>
);
}
export default DetailInfoWabahPenyakit;

View File

@@ -1,62 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Stack, SimpleGrid, Paper, Title, TextInput, Text, Button, Image } from '@mantine/core';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditInfoWabahPenyakit() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Edit Info Wabah/Penyakit</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Info Wabah/Penyakit</Text>}
placeholder='Masukkan nama Info Wabah/Penyakit'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Info Wabah/Penyakit</Text>}
placeholder='Masukkan deskripsi Info Wabah/Penyakit'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={4}>Preview Data Info Wabah/Penyakit</Title>
<Text fw={"bold"} fz={"sm"}>Nama Info Wabah/Penyakit</Text>
<Text fw={"bold"} fz={"sm"}>Deskripsi Info Wabah/Penyakit</Text>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<Text fw={"bold"} fz={"sm"}>Gambar</Text>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Stack>
</Box>
);
}
export default EditInfoWabahPenyakit;

View File

@@ -1,10 +1,13 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import JudulList from '../../_com/judulList';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import infoWabahPenyakit from '../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import { useShallowEffect } from '@mantine/hooks';
function InfoWabahPenyakit() {
return (
@@ -20,7 +23,20 @@ function InfoWabahPenyakit() {
}
function ListInfoWabahPenyakit() {
const infoWabahPenyakitState = useProxy(infoWabahPenyakit)
const router = useRouter()
useShallowEffect(() => {
infoWabahPenyakitState.findMany.load()
}, [])
if (!infoWabahPenyakitState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -34,27 +50,32 @@ function ListInfoWabahPenyakit() {
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Test</Text>
</Box></TableTd>
<TableTd>Test</TableTd>
<TableTd>
<Image w={100} src={"/"} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/kesehatan/info-wabah-penyakit/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
{infoWabahPenyakitState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.deskripsiSingkat}</Text>
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>

View File

@@ -0,0 +1,140 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 EditKontakDarurat() {
const kontakDaruratState = useProxy(kontakDarurat)
const router = useRouter();
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: kontakDaruratState.edit.form.name || '',
deskripsi: kontakDaruratState.edit.form.deskripsi || '',
imageId: kontakDaruratState.edit.form.imageId || '',
})
useEffect(() => {
const loadProgramKesehatan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await kontakDaruratState.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading program kesehatan:", error);
toast.error("Gagal memuat data program kesehatan");
}
};
loadProgramKesehatan();
}, [params?.id]);
const handleSubmit = async () => {
try {
kontakDaruratState.edit.form = {
...kontakDaruratState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
kontakDaruratState.edit.form.imageId = uploaded.id;
}
await kontakDaruratState.edit.update();
toast.success("Kontak darurat berhasil diperbarui!");
router.push("/admin/kesehatan/kontak-darurat");
} catch (error) {
console.error("Error updating kontak darurat:", error);
toast.error("Gagal memuat data kontak darurat");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Kontak Darurat</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack>
</Box >
);
}
export default EditKontakDarurat;

View File

@@ -0,0 +1,99 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Image, 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';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat';
function DetailKontakDarurat() {
const kontakDaruratState = useProxy(kontakDarurat)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const params = useParams()
useShallowEffect(() => {
kontakDaruratState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
kontakDaruratState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/kontak-darurat")
}
}
if (!kontakDaruratState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={400} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Kontak Darurat</Text>
{kontakDaruratState.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>{kontakDaruratState.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kontakDaruratState.findUnique.data.deskripsi }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={kontakDaruratState.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (kontakDaruratState.findUnique.data) {
setSelectedId(kontakDaruratState.findUnique.data.id)
setModalHapus(true)
}
}}
disabled={kontakDaruratState.delete.loading || !kontakDaruratState.findUnique.data}
>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/kontak-darurat/${kontakDaruratState.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus kontak darurat ini?"
/>
</Box>
);
}
export default DetailKontakDarurat;

View File

@@ -1,77 +1,113 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { useProxy } from 'valtio/utils';
import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat';
import { useState } from 'react';
import { toast } from 'react-toastify';
import ApiFetch from '@/lib/api-fetch';
import CreateEditor from '../../../_com/createEditor';
function CreateKontakDarurat() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Kontak Darurat</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder="masukkan kategori"
/>
{/* <FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/> */}
{/* {previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)} */}
<Box>
<Text fz="sm" fw="bold">Konten</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Button bg={colors['blue-button']}>
Simpan Potensi
const router = useRouter();
const kontakDaruratState = useProxy(kontakDarurat)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
kontakDaruratState.create.form = {
name: "",
deskripsi: "",
imageId: "",
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
kontakDaruratState.create.form.imageId = uploaded.id;
await kontakDaruratState.create.create();
resetForm();
router.push("/admin/kesehatan/kontak-darurat")
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Stack>
</Paper>
</Box>
);
</Box>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Kontak Darurat</Title>
<TextInput
value={kontakDaruratState.create.form.name}
onChange={(val) => {
kontakDaruratState.create.form.name = val.target.value;
}}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<CreateEditor
value={kontakDaruratState.create.form.deskripsi}
onChange={(val) => {
kontakDaruratState.create.form.deskripsi = val;
}}
/>
</Box>
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreateKontakDarurat;

View File

@@ -1,70 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailKontakDarurat() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Kontak Darurat</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Kontak Darurat</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Kontak Darurat</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>Test Deskripsi</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/kesehatan/kontak-darurat/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus penanganan darurat ini?"
/> */}
</Box>
);
}
export default DetailKontakDarurat;

View File

@@ -1,62 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Stack, SimpleGrid, Paper, Title, TextInput, Text, Button, Image } from '@mantine/core';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditKontakDarurat() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Edit Kontak Darurat</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kontak Darurat</Text>}
placeholder='Masukkan nama Kontak Darurat'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Kontak Darurat</Text>}
placeholder='Masukkan deskripsi Kontak Darurat'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={4}>Preview Data Kontak Darurat</Title>
<Text fw={"bold"} fz={"sm"}>Nama Kontak Darurat</Text>
<Text fw={"bold"} fz={"sm"}>Deskripsi Kontak Darurat</Text>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<Text fw={"bold"} fz={"sm"}>Gambar</Text>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Stack>
</Box>
);
}
export default EditKontakDarurat;

View File

@@ -1,66 +1,88 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import JudulList from '../../_com/judulList';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import kontakDarurat from '../../_state/kesehatan/kontak-darurat/kontakDarurat';
import { useShallowEffect } from '@mantine/hooks';
function KontakDarurat() {
return (
<Box>
<HeaderSearch
title='KontakDarurat'
title='Kontak Darurat'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListKontakDarurat/>
<ListKontakDarurat />
</Box>
);
}
function ListKontakDarurat() {
const kontakDaruratState = useProxy(kontakDarurat)
const router = useRouter();
useShallowEffect(() => {
kontakDaruratState.findMany.load()
}, [])
if (!kontakDaruratState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Kontak Darurat'
href='/admin/kesehatan/kontak-darurat/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Test</Text>
</Box></TableTd>
<TableTd>Test</TableTd>
<TableTd>
<Image w={100} src={"/"} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/kesehatan/kontak-darurat/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Kontak Darurat'
href='/admin/kesehatan/kontak-darurat/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{kontakDaruratState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/kontak-darurat/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}

View File

@@ -0,0 +1,140 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 EditPenangananDarurat() {
const penangananDaruratState = useProxy(penangananDarurat)
const router = useRouter();
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: penangananDaruratState.edit.form.name || '',
deskripsi: penangananDaruratState.edit.form.deskripsi || '',
imageId: penangananDaruratState.edit.form.imageId || '',
})
useEffect(() => {
const loadPenangananDarurat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await penangananDaruratState.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
})
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error('Error loading penanganan darurat:', error);
toast.error('Gagal memuat data penanganan darurat');
}
}
loadPenangananDarurat();
}, [params?.id])
const handleSubmit = async () => {
try {
penangananDaruratState.edit.form = {
...penangananDaruratState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
penangananDaruratState.edit.form.imageId = uploaded.id;
}
await penangananDaruratState.edit.update();
toast.success("Penanganan darurat berhasil diperbarui!");
router.push("/admin/kesehatan/penanganan-darurat");
} catch (error) {
console.error("Error updating penanganan darurat:", error);
toast.error("Gagal memuat data penanganan darurat");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"} w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Penanganan Darurat</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack >
</Box >
);
}
export default EditPenangananDarurat;

View File

@@ -0,0 +1,101 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { Skeleton } from '@mantine/core';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPenangananDarurat() {
const penangananDaruratState = useProxy(penangananDarurat)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const params = useParams()
useShallowEffect(() => {
penangananDaruratState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
penangananDaruratState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/penanganan-darurat")
}
}
if (!penangananDaruratState.findUnique.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Penanganan Darurat</Text>
{penangananDaruratState.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Penanganan Darurat</Text>
<Text fz={"lg"}>{penangananDaruratState.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: penangananDaruratState.findUnique.data.deskripsi }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={penangananDaruratState.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (penangananDaruratState.findUnique.data) {
setSelectedId(penangananDaruratState.findUnique.data.id)
setModalHapus(true)
}
}}
disabled={penangananDaruratState.delete.loading || !penangananDaruratState.findUnique.data}>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/penanganan-darurat/${penangananDaruratState.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus penanganan darurat ini?"
/>
</Box>
);
}
export default DetailPenangananDarurat;

View File

@@ -1,14 +1,56 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat';
function CreatePenangananDarurat() {
const router = useRouter();
const penangananDaruratState = useProxy(penangananDarurat)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
penangananDaruratState.create.form = {
name: "",
deskripsi: "",
imageId: "",
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
penangananDaruratState.create.form.imageId = uploaded.id;
await penangananDaruratState.create.create();
resetForm();
router.push("/admin/kesehatan/penanganan-darurat")
}
return (
<Box>
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
@@ -20,54 +62,48 @@ function CreatePenangananDarurat() {
<Title order={3}>Create Penanganan Darurat</Title>
<TextInput
value={penangananDaruratState.create.form.name}
onChange={(val) => {
penangananDaruratState.create.form.name = val.target.value;
}}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder="masukkan kategori"
/>
{/* <FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/> */}
{/* {previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)} */}
<Box>
<Text fz="sm" fw="bold">Konten</Text>
<KesehatanEditor
showSubmit={false}
<Text fz="sm" fw="bold">Deskripsi</Text>
<CreateEditor
value={penangananDaruratState.create.form.deskripsi}
onChange={(val) => {
penangananDaruratState.create.form.deskripsi = val;
}}
/>
</Box>
<Button bg={colors['blue-button']}>
Simpan Potensi
</Button>
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Box>

View File

@@ -1,70 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPenangananDarurat() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Penanganan Darurat</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Penanganan Darurat</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Penanganan Darurat</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>Test Deskripsi</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/kesehatan/penanganan-darurat/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus penanganan darurat ini?"
/> */}
</Box>
);
}
export default DetailPenangananDarurat;

View File

@@ -1,62 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Stack, SimpleGrid, Paper, Title, TextInput, Text, Button, Image } from '@mantine/core';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditPenangananDarurat() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Edit Penanganan Darurat</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Penanganan Darurat</Text>}
placeholder='Masukkan nama Penanganan Darurat'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Penanganan Darurat</Text>}
placeholder='Masukkan deskripsi Penanganan Darurat'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={4}>Preview Data Penanganan Darurat</Title>
<Text fw={"bold"} fz={"sm"}>Nama Penanganan Darurat</Text>
<Text fw={"bold"} fz={"sm"}>No Telp Penanganan Darurat</Text>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<Text fw={"bold"} fz={"sm"}>Pelayanan Posyandu</Text>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Stack>
</Box>
);
}
export default EditPenangananDarurat;

View File

@@ -1,10 +1,14 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import JudulList from '../../_com/judulList';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import penangananDarurat from '../../_state/kesehatan/penanganan-darurat/penangananDarurat';
function PenangananDarurat() {
return (
@@ -20,7 +24,21 @@ function PenangananDarurat() {
}
function ListPenangananDarurat() {
const penangananDaruratState = useProxy(penangananDarurat)
const router = useRouter();
useShallowEffect(() => {
penangananDaruratState.findMany.load()
}, [])
if (!penangananDaruratState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -34,27 +52,31 @@ function ListPenangananDarurat() {
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
{penangananDaruratState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Test</Text>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box></TableTd>
<TableTd>Test</TableTd>
<TableTd>
<Image w={100} src={"/"} alt="image" />
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/kesehatan/penanganan-darurat/detail')}>
<Image w={100} src={item.image?.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/penanganan-darurat/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>

View File

@@ -0,0 +1,147 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 EditPosyandu() {
const statePosyandu = useProxy(posyandustate)
const router = useRouter();
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: statePosyandu.edit.form.name || '',
nomor: statePosyandu.edit.form.nomor || '',
deskripsi: statePosyandu.edit.form.deskripsi || '',
imageId: statePosyandu.edit.form.imageId || '',
});
useEffect(() => {
const loadPosyandu = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await statePosyandu.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
nomor: data.nomor || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading posyandu:", error);
toast.error("Gagal memuat data posyandu");
}
}
loadPosyandu();
}, [params?.id])
const handleSubmit = async () => {
try {
statePosyandu.edit.form = {
...statePosyandu.edit.form,
name: formData.name,
nomor: formData.nomor,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
statePosyandu.edit.form.imageId = uploaded.id;
}
await statePosyandu.edit.update();
toast.success("Posyandu berhasil diperbarui!");
router.push("/admin/kesehatan/posyandu");
} catch (error) {
console.error("Error updating posyandu:", error);
toast.error("Gagal memuat data posyandu");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Posyandu</Title>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>}
placeholder='Masukkan nama posyandu'
/>
<TextInput
value={formData.nomor}
onChange={(e) => setFormData({ ...formData, nomor: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nomor Posyandu</Text>}
placeholder='Masukkan nomor posyandu'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Posyandu</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
statePosyandu.edit.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPosyandu;

View File

@@ -0,0 +1,101 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import posyandustate from '../../../_state/kesehatan/posyandu/posyandu';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPosyandu() {
const statePosyandu = useProxy(posyandustate)
const params = useParams()
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
statePosyandu.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
statePosyandu.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/posyandu")
}
}
if (!statePosyandu.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Posyandu</Text>
{statePosyandu.findUnique.data ? (
<Paper key={statePosyandu.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Posyandu</Text>
<Text fz={"lg"}>{statePosyandu.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nomor Posyandu</Text>
<Text fz={"lg"}>{statePosyandu.findUnique.data.nomor}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Posyandu</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePosyandu.findUnique.data.deskripsi }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={statePosyandu.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button onClick={() => {
if (statePosyandu.findUnique.data) {
setSelectedId(statePosyandu.findUnique.data.id)
setModalHapus(true)
}
}} color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/posyandu/${statePosyandu.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus posyandu ini?"
/>
</Box>
);
}
export default DetailPosyandu;

View File

@@ -1,13 +1,59 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import posyandustate from '../../../_state/kesehatan/posyandu/posyandu';
function CreatePosyandu() {
const statePosyandu = useProxy(posyandustate)
const router = useRouter();
const [file, setFile] = useState<File | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const resetForm = () => {
statePosyandu.create.form = {
name: "",
nomor: "",
deskripsi: "",
imageId: "",
};
setFile(null);
setPreviewImage(null);
}
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
statePosyandu.create.form.imageId = uploaded.id;
await statePosyandu.create.create();
resetForm();
router.push("/admin/kesehatan/posyandu")
}
return (
<Box>
<Box mb={10}>
@@ -19,26 +65,52 @@ function CreatePosyandu() {
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Posyandu</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>}
placeholder='Masukkan nama posyandu'
value={statePosyandu.create.form.name}
onChange={(e) => {
statePosyandu.create.form.name = e.target.value;
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nomor Posyandu</Text>}
placeholder='Masukkan nomor posyandu'
value={statePosyandu.create.form.nomor}
onChange={(e) => {
statePosyandu.create.form.nomor = e.target.value;
}}
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Posyandu</Text>
<KesehatanEditor
showSubmit={false}
<CreateEditor
value={statePosyandu.create.form.deskripsi}
onChange={(htmlContent) => {
statePosyandu.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,66 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPosyandu() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Posyandu</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Posyandu</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nomor Posyandu</Text>
<Text fz={"lg"}>089647038426</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Posyandu</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/kesehatan/posyandu/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus penanganan darurat ini?"
/> */}
</Box>
);
}
export default DetailPosyandu;

View File

@@ -1,49 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
function EditPosyandu() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Posyandu</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>}
placeholder='Masukkan nama posyandu'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nomor Posyandu</Text>}
placeholder='Masukkan nomor posyandu'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Posyandu</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPosyandu;

View File

@@ -1,10 +1,13 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import posyandustate from '../../_state/kesehatan/posyandu/posyandu';
import { useShallowEffect } from '@mantine/hooks';
function Posyandu() {
return (
@@ -14,13 +17,27 @@ function Posyandu() {
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListPosyandu/>
<ListPosyandu />
</Box>
);
}
function ListPosyandu() {
const statePosyandu = useProxy(posyandustate)
const router = useRouter();
useShallowEffect(() => {
statePosyandu.findMany.load()
}, [])
if (!statePosyandu.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -28,28 +45,34 @@ function ListPosyandu() {
title='List Posyandu'
href='/admin/kesehatan/posyandu/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Posyandu</TableTh>
<TableTh>Nomor Posyandu</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Posyandu</TableTh>
<TableTh>Nomor Posyandu</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Posyandu 1</TableTd>
<TableTd>0896232831883</TableTd>
<TableTd>Posyandu 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/kesehatan/posyandu/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{statePosyandu.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.nomor}</TableTd>
<TableTd>
<Text fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/posyandu/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
</Box>
);

View File

@@ -0,0 +1,150 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } 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 EditProgramKesehatan() {
const programKesehatanState = useProxy(programKesehatan)
const router = useRouter();
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: programKesehatanState.edit.form.name || '',
deskripsiSingkat: programKesehatanState.edit.form.deskripsiSingkat || '',
deskripsi: programKesehatanState.edit.form.deskripsi || '',
imageId: programKesehatanState.edit.form.imageId || '',
})
useEffect(() => {
const loadProgramKesehatan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await programKesehatanState.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsiSingkat: data.deskripsiSingkat || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading program kesehatan:", error);
toast.error("Gagal memuat data program kesehatan");
}
};
loadProgramKesehatan();
}, [params?.id]);
const handleSubmit = async () => {
try {
programKesehatanState.edit.form = {
...programKesehatanState.edit.form,
name: formData.name,
deskripsiSingkat: formData.deskripsiSingkat,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
programKesehatanState.edit.form.imageId = uploaded.id;
}
await programKesehatanState.edit.update();
toast.success("Program kesehatan berhasil diperbarui!");
router.push("/admin/kesehatan/program-kesehatan");
} catch (error) {
console.error("Error updating program kesehatan:", error);
toast.error("Gagal memuat data program kesehatan");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Program Kesehatan</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
value={formData.deskripsiSingkat}
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
</Box>
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
'data:image/png;base64,' + Buffer.from(buf).toString('base64')
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Stack>
</Box >
);
}
export default EditProgramKesehatan;

View File

@@ -0,0 +1,103 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKesehatan() {
const programKesehatanState = useProxy(programKesehatan)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const params = useParams()
useShallowEffect(() => {
programKesehatanState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
programKesehatanState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/program-kesehatan")
}
}
if (!programKesehatanState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={400} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Kesehatan</Text>
{programKesehatanState.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>{programKesehatanState.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>{programKesehatanState.findUnique.data.deskripsiSingkat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: programKesehatanState.findUnique.data.deskripsi }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={programKesehatanState.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (programKesehatanState.findUnique.data) {
setSelectedId(programKesehatanState.findUnique.data.id)
setModalHapus(true)
}
}}
disabled={programKesehatanState.delete.loading || !programKesehatanState.findUnique.data}
>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/program-kesehatan/${programKesehatanState.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kesehatan ini?"
/>
</Box>
);
}
export default DetailProgramKesehatan;

View File

@@ -1,43 +1,101 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan';
function CreateProgramKesehatan() {
const router = useRouter();
const programKesehatanState = useProxy(programKesehatan)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const resetForm = () => {
// Reset state di valtio
programKesehatanState.create.form = {
name: "",
deskripsiSingkat: "",
deskripsi: "",
imageId: "",
};
// Reset state lokal
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Simpan ID gambar ke form
programKesehatanState.create.form.imageId = uploaded.id;
// Submit data berita
await programKesehatanState.create.create();
// Reset form setelah submit
resetForm();
router.push("/admin/kesehatan/program-kesehatan")
};
return (
<Box>
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Program Kesehatan</Title>
<TextInput
value={programKesehatanState.create.form.name}
onChange={(val) => {
programKesehatanState.create.form.name = val.target.value;
}}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
value={programKesehatanState.create.form.deskripsiSingkat}
onChange={(val) => {
programKesehatanState.create.form.deskripsiSingkat = val.target.value;
}}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder="masukkan kategori"
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<CreateEditor
value={programKesehatanState.create.form.deskripsi}
onChange={(val) => {
programKesehatanState.create.form.deskripsi = val;
}}
/>
</Box>
{/* <FileInput
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
@@ -48,25 +106,18 @@ function CreateProgramKesehatan() {
);
setPreviewImage(base64);
}}
/> */}
/>
{/* {previewImage ? (
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)} */}
)}
<Box>
<Text fz="sm" fw="bold">Konten</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Button bg={colors['blue-button']}>
Simpan Potensi
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>

View File

@@ -1,70 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKesehatan() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Potensi</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>Test Deskripsi</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/kesehatan/program-kesehatan/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailProgramKesehatan;

View File

@@ -1,64 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Stack, SimpleGrid, Paper, Title, TextInput, Text, Button } from '@mantine/core';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditProgramKesehatan() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap={"xs"}>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={3}>Edit Program Kesehatan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Program Kesehatan</Text>}
placeholder='Masukkan nama Program Kesehatan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>No Telp Program Kesehatan</Text>}
placeholder='Masukkan no telp Program Kesehatan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Pelayanan Posyandu</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
</Stack>
</Paper>
</Box>
<Box>
<Paper bg={colors['white-1']} p={"md"}>
<Stack gap={"xs"}>
<Title order={4}>Preview Data Program Kesehatan</Title>
<Text fw={"bold"} fz={"sm"}>Nama Program Kesehatan</Text>
<Text fw={"bold"} fz={"sm"}>No Telp Program Kesehatan</Text>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<Text fw={"bold"} fz={"sm"}>Pelayanan Posyandu</Text>
</Stack>
</Paper>
</Box>
</SimpleGrid>
</Stack>
</Box>
);
}
export default EditProgramKesehatan;

View File

@@ -1,66 +1,86 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import JudulList from '../../_com/judulList';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import programKesehatan from '../../_state/kesehatan/program-kesehatan/programKesehatan';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
function ProgramKesehatan() {
return (
<Box>
<HeaderSearch
title='ProgramKesehatan'
title='Program Kesehatan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListProgramKesehatan/>
<ListProgramKesehatan />
</Box>
);
}
function ListProgramKesehatan() {
const programKesehatanState = useProxy(programKesehatan)
const router = useRouter()
useShallowEffect(() => {
programKesehatanState.findMany.load()
}, [])
if (!programKesehatanState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Program Kesehatan'
href='/admin/kesehatan/program-kesehatan/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Test</Text>
</Box></TableTd>
<TableTd>Test</TableTd>
<TableTd>
<Image w={100} src={"/"} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/kesehatan/program-kesehatan/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Program Kesehatan'
href='/admin/kesehatan/program-kesehatan/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh w={250}>Judul</TableTh>
<TableTh w={250}>Deskripsi Singkat</TableTh>
<TableTh w={250}>Image</TableTh>
<TableTh w={200}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{programKesehatanState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>{item.deskripsiSingkat}</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/program-kesehatan/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}

View File

@@ -0,0 +1,286 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { ChangeEvent, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface PuskesmasFormBase {
name: string;
alamat: string;
jam: {
workDays: string;
weekDays: string;
holiday: string;
};
kontak: {
kontakPuskesmas: string;
email: string;
facebook: string;
kontakUGD: string;
};
imageId: string;
}
interface PuskesmasFormData extends PuskesmasFormBase {
image?: {
link: string;
};
}
function EditPuskesmas() {
const statePuskesmas = useProxy(puskesmasState);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState<PuskesmasFormData>({
name: statePuskesmas.edit.form.name || '',
alamat: statePuskesmas.edit.form.alamat || '',
jam: {
workDays: statePuskesmas.edit.form.jam?.workDays || '',
weekDays: statePuskesmas.edit.form.jam?.weekDays || '',
holiday: statePuskesmas.edit.form.jam?.holiday || '',
},
kontak: {
kontakPuskesmas: statePuskesmas.edit.form.kontak?.kontakPuskesmas || '',
email: statePuskesmas.edit.form.kontak?.email || '',
facebook: statePuskesmas.edit.form.kontak?.facebook || '',
kontakUGD: statePuskesmas.edit.form.kontak?.kontakUGD || '',
},
imageId: statePuskesmas.edit.form.imageId || '',
});
useEffect(() => {
const loadPuskesmas = async () => {
const id = params?.id as string;
if (!id) return;
try {
await statePuskesmas.edit.load(id);
const { form } = statePuskesmas.edit;
if (form) {
setFormData({
name: form.name,
alamat: form.alamat,
jam: {
workDays: form.jam.workDays,
weekDays: form.jam.weekDays,
holiday: form.jam.holiday,
},
kontak: {
kontakPuskesmas: form.kontak.kontakPuskesmas,
email: form.kontak.email,
facebook: form.kontak.facebook,
kontakUGD: form.kontak.kontakUGD,
},
imageId: form.imageId,
});
// Check if there's an existing image URL in the form data
const formWithImage = form as PuskesmasFormData;
if (formWithImage.image?.link) {
setPreviewImage(formWithImage.image.link);
}
}
} catch (error) {
console.error("Error loading puskesmas:", error);
toast.error("Gagal memuat data puskesmas");
}
};
loadPuskesmas();
}, [params?.id]);
const handleSubmit = async () => {
try {
statePuskesmas.edit.form = {
...statePuskesmas.edit.form,
name: formData.name,
alamat: formData.alamat,
jam: {
workDays: formData.jam.workDays,
weekDays: formData.jam.weekDays,
holiday: formData.jam.holiday,
},
kontak: {
kontakPuskesmas: formData.kontak.kontakPuskesmas,
email: formData.kontak.email,
facebook: formData.kontak.facebook,
kontakUGD: formData.kontak.kontakUGD,
},
imageId: formData.imageId,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload gambar");
return;
}
statePuskesmas.edit.form.imageId = uploaded.id;
}
const success = await statePuskesmas.edit.submit();
if (success) {
toast.success("Puskesmas berhasil diperbarui!");
router.push("/admin/kesehatan/puskesmas");
}
} catch (error) {
console.error("Error updating puskesmas:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data puskesmas");
}
};
const handleFileChange = (selectedFile: File | null) => {
if (selectedFile) {
setFile(selectedFile);
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result) {
setPreviewImage(e.target.result as string);
}
};
reader.readAsDataURL(selectedFile);
} else {
setFile(null);
setPreviewImage(null);
}
};
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleNestedChange = (section: 'jam' | 'kontak', field: string, value: string) => {
setFormData(prev => ({
...prev,
[section]: {
...prev[section],
[field]: value
}
}));
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant="subtle" color="blue">
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Stack gap="xs">
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Edit Puskesmas</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama Puskesmas</Text>}
placeholder="masukkan nama puskesmas"
name="name"
value={formData.name}
onChange={handleInputChange}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
name="alamat"
value={formData.alamat}
onChange={handleInputChange}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Buka</Text>}
placeholder="masukkan jam buka"
value={formData.jam.workDays}
onChange={(e) => handleNestedChange('jam', 'workDays', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Tutup</Text>}
placeholder="masukkan jam tutup"
value={formData.jam.weekDays}
onChange={(e) => handleNestedChange('jam', 'weekDays', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Libur</Text>}
placeholder="masukkan jam libur"
value={formData.jam.holiday}
onChange={(e) => handleNestedChange('jam', 'holiday', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Kontak Puskesmas</Text>}
placeholder="masukkan kontak puskesmas"
value={formData.kontak.kontakPuskesmas}
onChange={(e) => handleNestedChange('kontak', 'kontakPuskesmas', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Email</Text>}
placeholder="masukkan email"
value={formData.kontak.email}
onChange={(e) => handleNestedChange('kontak', 'email', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Facebook</Text>}
placeholder="masukkan facebook"
value={formData.kontak.facebook}
onChange={(e) => handleNestedChange('kontak', 'facebook', e.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Kontak UGD</Text>}
placeholder="masukkan kontak UGD"
value={formData.kontak.kontakUGD}
onChange={(e) => handleNestedChange('kontak', 'kontakUGD', e.target.value)}
/>
<FileInput
placeholder="Pilih gambar"
label="Gambar"
accept="image/*"
leftSection={<IconImageInPicture size={16} />}
value={file}
onChange={handleFileChange}
/>
{previewImage ? (
<Image alt="Preview" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)}
<Button
onClick={handleSubmit}
bg={colors['blue-button']}
loading={statePuskesmas.edit.loading}
>
Simpan Perubahan
</Button>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default EditPuskesmas;

View File

@@ -0,0 +1,111 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPuskesmas() {
const params = useParams()
const router = useRouter();
const statePuskesmas = useProxy(puskesmasState)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
statePuskesmas.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
statePuskesmas.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/puskesmas")
}
}
if (!statePuskesmas.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Puskesmas</Text>
{statePuskesmas.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Puskesmas</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.alamat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jam Operasional</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.jam.workDays}</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.jam.weekDays}</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.jam.holiday}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={statePuskesmas.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kontak</Text>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.kontakPuskesmas}</Text>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.email}</Text>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.facebook}</Text>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.kontakUGD}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (statePuskesmas.findUnique.data) {
setSelectedId(statePuskesmas.findUnique.data.id)
setModalHapus(true)
}
}}>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/puskesmas/${statePuskesmas.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/>
</Box>
);
}
export default DetailPuskesmas;

View File

@@ -1,14 +1,76 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas';
function CreatePuskesmas() {
const statePuskesmas = useProxy(puskesmasState)
const router = useRouter();
const [file, setFile] = useState<File | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const resetForm = () => {
statePuskesmas.create.form = {
name: "",
alamat: "",
jam: {
workDays: "",
weekDays: "",
holiday: "",
},
kontak: {
kontakPuskesmas: "",
email: "",
facebook: "",
kontakUGD: "",
},
imageId: "",
image: undefined,
};
setFile(null);
setPreviewImage(null);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
statePuskesmas.create.form.imageId = uploaded.id;
// State is already being updated directly in the form inputs
await statePuskesmas.create.submit();
toast.success("Data berhasil disimpan");
resetForm();
// After successful submission, redirect to the list page
router.push('/admin/kesehatan/puskesmas');
}
return (
<Box>
<Box component="form" onSubmit={handleSubmit}>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
@@ -18,26 +80,80 @@ function CreatePuskesmas() {
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Title order={3}>Create Puskesmas</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
label={<Text fz="sm" fw="bold">Nama Puskesmas</Text>}
placeholder="masukkan nama puskesmas"
value={statePuskesmas.create.form.name}
onChange={(e) => {
statePuskesmas.create.form.name = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
value={statePuskesmas.create.form.alamat}
onChange={(e) => {
statePuskesmas.create.form.alamat = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Buka</Text>}
placeholder="masukkan jam buka"
value={statePuskesmas.create.form.jam.workDays}
onChange={(e) => {
statePuskesmas.create.form.jam.workDays = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Tutup</Text>}
placeholder="masukkan jam tutup"
value={statePuskesmas.create.form.jam.weekDays}
onChange={(e) => {
statePuskesmas.create.form.jam.weekDays = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Holiday</Text>}
placeholder="masukkan holiday"
value={statePuskesmas.create.form.jam.holiday}
onChange={(e) => {
statePuskesmas.create.form.jam.holiday = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Kontak Puskesmas</Text>}
placeholder="masukkan kontak puskesmas"
value={statePuskesmas.create.form.kontak.kontakPuskesmas}
onChange={(e) => {
statePuskesmas.create.form.kontak.kontakPuskesmas = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Email</Text>}
placeholder="masukkan email"
value={statePuskesmas.create.form.kontak.email}
onChange={(e) => {
statePuskesmas.create.form.kontak.email = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Facebook</Text>}
placeholder="masukkan facebook"
value={statePuskesmas.create.form.kontak.facebook}
onChange={(e) => {
statePuskesmas.create.form.kontak.facebook = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Kontak UGD</Text>}
placeholder="masukkan kontak ugd"
value={statePuskesmas.create.form.kontak.kontakUGD}
onChange={(e) => {
statePuskesmas.create.form.kontak.kontakUGD = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<TextInput
label={<Text fz="sm" fw="bold">Kategori</Text>}
placeholder="masukkan kategori"
/>
{/* <FileInput
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</Text>}
value={file}
onChange={async (e) => {
@@ -48,25 +164,18 @@ function CreatePuskesmas() {
);
setPreviewImage(base64);
}}
/> */}
/>
{/* {previewImage ? (
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg="gray">
<IconImageInPicture />
</Center>
)} */}
)}
<Box>
<Text fz="sm" fw="bold">Konten</Text>
<KesehatanEditor
showSubmit={false}
/>
</Box>
<Button bg={colors['blue-button']}>
Simpan Potensi
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Puskesmas
</Button>
</Stack>
</Paper>

View File

@@ -1,70 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPuskesmas() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Potensi</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
<Text fz={"lg"}>Test Kategori</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>Test Deskripsi</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Konten</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/kesehatan/puskesmas/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailPuskesmas;

View File

@@ -1,10 +1,13 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import JudulList from '../../_com/judulList';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import puskesmasState from '../../_state/kesehatan/puskesmas/puskesmas';
function Puskesmas() {
return (
@@ -20,7 +23,20 @@ function Puskesmas() {
}
function ListPuskesmas() {
const statePuskesmas = useProxy(puskesmasState)
const router = useRouter();
useShallowEffect(() => {
statePuskesmas.findMany.load()
}, [])
if (!statePuskesmas.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500}/>
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -33,28 +49,27 @@ function ListPuskesmas() {
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Nama Puskesmas</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Test</Text>
</Box></TableTd>
<TableTd>Test</TableTd>
<TableTd>
<Image w={100} src={"/"} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/kesehatan/puskesmas/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
{statePuskesmas.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Image w={100} src={item.image.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/puskesmas/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>

View File

@@ -12,7 +12,7 @@ function Page() {
const router = useRouter()
const allList = useProxy(stateProfilePPID)
useShallowEffect(() => {
allList.profile.load("1") // Assuming "1" is your default ID, adjust as needed
allList.profile.load("edit") // Assuming "1" is your default ID, adjust as needed
}, [])
if (!allList.profile.data) {
@@ -45,7 +45,7 @@ function Page() {
<Grid>
<GridCol span={{ base: 12, md: 12 }}>
<Center>
<Image src={"/api/img/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
</Center>
</GridCol>
<GridCol span={{ base: 12, md: 12 }}>
@@ -62,7 +62,7 @@ function Page() {
<Center>
<Image
pt={{ base: 0, md: 90 }}
src={item.image?.link}
src={item.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }}
alt='Foto Profil PPID'
onError={(e) => {

View File

@@ -42,6 +42,7 @@ function VisiMisiPPIDEdit() {
visiMisi.findById.data.misi = draftMisi;
visiMisi.update.save(visiMisi.findById.data);
}
router.push('/admin/ppid/visi-misi-ppid')
};
return (

View File

@@ -51,7 +51,7 @@ function VisiMisiPPIDList() {
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box pb={30}>
<Center>
<Image src={"/api/img/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
<Image src={"/darmasaba-icon.png"} w={{ base: 100, md: 150 }} alt='' />
</Center>
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} fw={"bold"}>
MOTO PPID DESA DARMASABA

View File

@@ -11,41 +11,26 @@ export const navBar = [
},
{
id: "Landing_Page_2",
name: "Penghargaan",
path: "/admin/landing-page/penghargaan"
},
{
id: "Landing_Page_3",
name: "Layanan",
path: "/admin/landing-page/layanan"
},
{
id: "Landing_Page_4",
name: "Potensi",
path: "/admin/landing-page/potensi"
},
{
id: "Landing_Page_5",
name: "Desa Anti Korupsi",
path: "/admin/landing-page/desa-anti-korupsi"
},
{
id: "Landing_Page_6",
id: "Landing_Page_3",
name: "Indeks Kepuasan Masyarakat",
path: "/admin/landing-page/indeks-kepuasan-masyarakat"
},
{
id: "Landing_Page_7",
id: "Landing_Page_4",
name: "SDGs Desa",
path: "/admin/landing-page/sdgs-desa"
},
{
id: "Landing_Page_8",
id: "Landing_Page_5",
name: "APBDes",
path: "/admin/landing-page/apbdes"
},
{
id: "Landing_Page_9",
id: "Landing_Page_6",
name: "Prestasi Desa",
path: "/admin/landing-page/prestasi-desa"
}
@@ -108,7 +93,7 @@ export const navBar = [
{
id: "Desa_1",
name: "Profile",
path: "/admin/desa/profile"
path: "/admin/desa/profile/profile-desa"
},
{
id: "Desa_2",

View File

@@ -0,0 +1,49 @@
import prisma from "@/lib/prisma";
export default async function profilePerbekelFindById(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, { status: 400 });
}
const data = await prisma.profilPerbekel.findUnique({
where: { id },
include: {
image: true,
}
});
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Data berhasil ditemukan",
data,
}, { status: 200 });
} catch (error) {
console.error("Error fetching profile Perbekel:", error);
return Response.json({
success: false,
message: "Terjadi kesalahan saat mengambil data profile Perbekel",
}, { status: 500 });
}
}

View File

@@ -0,0 +1,32 @@
import Elysia, { t } from "elysia";
import profilePerbekelFindById from "./find-by-id";
import profilePerbekelUpdate from "./update";
const ProfilPerbekel = new Elysia({
prefix: "/profileperbekel",
tags: ["Desa/Profile"],
})
.get("/:id", async (context) => {
const response = await profilePerbekelFindById(
new Request(context.request)
);
return response;
})
.put(
"/:id",
async (context) => {
const response = await profilePerbekelUpdate(context);
return response;
},
{
body: t.Object({
biodata: t.String(),
pengalaman: t.String(),
pengalamanOrganisasi: t.String(),
programUnggulan: t.String(),
imageId: t.String(),
}),
}
);
export default ProfilPerbekel;

View File

@@ -1,33 +1,118 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
import path from "path";
import fs from "fs/promises";
type FormCreate = Prisma.ProfilPerbekelGetPayload<{
select: {
id: true;
biodata: true;
pengalaman: true;
pengalamanOrganisasi: true;
programUnggulan: true;
}
}>
type FormUpdate = Prisma.ProfilPerbekelGetPayload<{
select: {
id: true;
biodata: true;
pengalaman: true;
pengalamanOrganisasi: true;
programUnggulan: true;
imageId: true;
};
}>;
export default async function profilePerbekelUpdate(context: Context) {
const body = context.body as FormCreate;
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
await prisma.profilPerbekel.update({
where: {
id: body.id
},
const { biodata, pengalaman, pengalamanOrganisasi, programUnggulan, imageId } = body;
if (!id) {
return new Response(
JSON.stringify({
success: false,
message: "ID tidak boleh kosong",
}),
{
status: 400,
headers: {
"Content-Type": "application/json",
},
}
);
}
const exisitng = await prisma.profilPerbekel.findUnique({
where: {
id,
},
include: {
image: true,
},
});
if (!exisitng) {
return new Response(
JSON.stringify({
success: false,
message: "Data tidak ditemukan",
}),
{
status: 404,
headers: {
"Content-Type": "application/json",
},
}
);
}
if (exisitng.imageId !== imageId) {
const oldImage = exisitng.image;
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: oldImage.id },
});
} catch (error) {
console.error("Gagal hapus gambar lama:", error);
}
}
}
const updated = await prisma.profilPerbekel.update({
where: {id},
data: {
biodata: body.biodata,
pengalaman: body.pengalaman,
pengalamanOrganisasi: body.pengalamanOrganisasi,
programUnggulan: body.programUnggulan,
biodata,
pengalaman,
pengalamanOrganisasi,
programUnggulan,
imageId,
}
})
return {
success: true,
message: "Profile Perbekel Berhasil Diupdate",
}
}
return new Response(
JSON.stringify({
success: true,
message: "Data berhasil diperbarui",
data: updated,
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
);
} catch (error) {
console.error("Error updating profile Perbekel:", error);
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate profile Perbekel",
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
},
}
);
}
}

View File

@@ -1,33 +0,0 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function profileDesaFindById(context: Context) {
try {
const id = context?.params?.slugs?.[0];
// If no ID provided, get the first profile
if (!id) {
const data = await prisma.profileDesa.findFirst();
return {
success: true,
data,
};
}
const data = await prisma.profileDesa.findUniqueOrThrow({
where: { id },
});
return {
success: true,
data,
};
} catch (error) {
console.error("Error fetching profileDesa:", error);
return {
success: false,
message: error instanceof Error ? error.message : "Unknown error",
};
}
}

View File

@@ -1,51 +1,18 @@
import Elysia, { t } from "elysia";
import lambangDesaUpdate from "./lambangDesa/update";
import maskotDesaUpdate from "./maskotDesa/update";
import profilePerbekelUpdate from "../profilePerbekel/update";
import sejarahDesaUpdate from "./sejarah/update";
import visimisiDesaUpdate from "./visimisiDesa/update";
import profileDesaFindById from "./find-by-id";
import SejarahDesa from "./sejarah";
import VisiMisiDesa from "./visi-misi";
import LambangDesa from "./lambang-desa";
import MaskotDesa from "./maskot-desa";
import Elysia from "elysia";
import ProfilPerbekel from "../profilePerbekel";
const ProfileDesa = new Elysia({
prefix: "/profile",
tags: ["Desa/Profile"]
})
.get("/find-by-id", profileDesaFindById)
.post("/profilePerbekel/update", profilePerbekelUpdate, {
body: t.Object({
id: t.String(),
biodata: t.String(),
pengalaman: t.String(),
pengalamanOrganisasi: t.String(),
programUnggulan: t.String(),
})
})
.post("/visimisiDesa/update", visimisiDesaUpdate, {
body: t.Object({
id: t.String(),
visi: t.String(),
misi: t.String(),
})
})
.post("/sejarah/update", sejarahDesaUpdate, {
body: t.Object({
id: t.String(),
sejarah: t.String(),
})
})
.post("/lambangDesa/update", lambangDesaUpdate, {
body: t.Object({
id: t.String(),
lambang: t.String(),
})
})
.post("/maskotDesa/update", maskotDesaUpdate, {
body: t.Object({
id: t.String(),
maskot: t.String(),
})
prefix: "/profile",
tags: ["Desa/Profile"],
})
.use(SejarahDesa)
.use(VisiMisiDesa)
.use(LambangDesa)
.use(MaskotDesa)
.use(ProfilPerbekel);
export default ProfileDesa
export default ProfileDesa;

View File

@@ -0,0 +1,60 @@
import prisma from "@/lib/prisma";
export default async function lambangDesaFindById(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split("/");
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json(
{
success: false,
message: "ID tidak boleh kosong",
},
{ status: 400 }
);
}
try {
if (typeof id !== "string") {
return Response.json(
{
success: false,
message: "ID tidak valid",
},
{ status: 400 }
);
}
const data = await prisma.lambangDesa.findUnique({
where: { id },
});
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 lambang desa:", error);
return Response.json(
{
success: false,
message: "Terjadi kesalahan saat mengambil data",
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,28 @@
import Elysia, { t } from "elysia";
import lambangDesaFindById from "./find-by-id";
import lambangDesaUpdate from "./update";
const LambangDesa = new Elysia({
prefix: "/lambang",
tags: ["Desa/Profile"],
})
.get("/:id", async (context) => {
const response = await lambangDesaFindById(new Request(context.request));
return response;
})
.put(
"/:id",
async (context) => {
const response = await lambangDesaUpdate(context);
return response;
},
{
body: t.Object({
judul: t.String(),
deskripsi: t.String(),
}),
}
);
export default LambangDesa;

View File

@@ -0,0 +1,50 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function lambangDesaUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = await context.body as {
judul: string;
deskripsi: string;
};
if (!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak boleh kosong",
}), { status: 400 });
}
const existing = await prisma.lambangDesa.findUnique({
where: { id },
});
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Data tidak ditemukan",
}), { status: 404 });
}
const updated = await prisma.lambangDesa.update({
where: { id },
data: {
judul: body.judul,
deskripsi: body.deskripsi,
},
});
return new Response(JSON.stringify({
success: true,
message: "Berhasil memperbarui data",
data: updated,
}), { status: 200 });
} catch (error) {
console.error("Update error:", error);
return new Response(JSON.stringify({
success: false,
message: "Gagal memperbarui data: " + (error instanceof Error ? error.message : 'Unknown error'),
}), { status: 500 });
}
}

View File

@@ -1,28 +0,0 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.ProfileDesaGetPayload<{
select: {
id: true;
lambang: true;
}
}>
export default async function lambangDesaUpdate(context: Context) {
const body = context.body as FormCreate;
await prisma.profileDesa.update({
where: {
id: body.id
},
data: {
lambang: body.lambang,
}
})
return {
success: true,
message: "Profile Desa Berhasil Diupdate",
}
}

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