Compare commits

...

16 Commits

192 changed files with 12894 additions and 2271 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -15,7 +15,7 @@
"dependencies": {
"@cubejs-client/core": "^0.31.0",
"@elysiajs/cors": "^1.2.0",
"@elysiajs/eden": "^1.2.0",
"@elysiajs/eden": "^1.3.2",
"@elysiajs/static": "^1.3.0",
"@elysiajs/stream": "^1.1.0",
"@elysiajs/swagger": "^1.2.0",
@@ -46,7 +46,7 @@
"bun": "^1.2.2",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"elysia": "^1.2.12",
"elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
"form-data": "^4.0.2",

View File

@@ -0,0 +1,10 @@
[
{
"id": "4b95bge6-012e-5ged-9552-4d8g65d44959",
"nama": "Makanan"
},
{
"id": "5c06chf7-123f-6hfe-0663-5e9h76e55060",
"nama": "Minuman"
}
]

View File

@@ -0,0 +1,68 @@
/*
Warnings:
- The primary key for the `DataKematian_Kelahiran` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `uuid` on the `DataKematian_Kelahiran` table. All the data in the column will be lost.
- The primary key for the `GrafikKepuasan` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `uuid` on the `GrafikKepuasan` table. All the data in the column will be lost.
- A unique constraint covering the columns `[id]` on the table `DataKematian_Kelahiran` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[id]` on the table `GrafikKepuasan` will be added. If there are existing duplicate values, this will fail.
- Added the required column `doctorSignId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
- Added the required column `firstAidId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
- Added the required column `introductionId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
- Added the required column `mythVsFactId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
- Added the required column `preventionId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
- Added the required column `symptomId` to the `ArtikelKesehatan` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "DataKematian_Kelahiran_uuid_key";
-- DropIndex
DROP INDEX "GrafikKepuasan_uuid_key";
-- AlterTable
ALTER TABLE "ArtikelKesehatan" ADD COLUMN "doctorSignId" TEXT NOT NULL,
ADD COLUMN "firstAidId" TEXT NOT NULL,
ADD COLUMN "introductionId" TEXT NOT NULL,
ADD COLUMN "mythVsFactId" TEXT NOT NULL,
ADD COLUMN "preventionId" TEXT NOT NULL,
ADD COLUMN "symptomId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "DataKematian_Kelahiran" DROP CONSTRAINT "DataKematian_Kelahiran_pkey",
DROP COLUMN "uuid",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT;
DROP SEQUENCE "DataKematian_Kelahiran_id_seq";
-- AlterTable
ALTER TABLE "GrafikKepuasan" DROP CONSTRAINT "GrafikKepuasan_pkey",
DROP COLUMN "uuid",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT;
DROP SEQUENCE "GrafikKepuasan_id_seq";
-- CreateIndex
CREATE UNIQUE INDEX "DataKematian_Kelahiran_id_key" ON "DataKematian_Kelahiran"("id");
-- CreateIndex
CREATE UNIQUE INDEX "GrafikKepuasan_id_key" ON "GrafikKepuasan"("id");
-- AddForeignKey
ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_introductionId_fkey" FOREIGN KEY ("introductionId") REFERENCES "Introduction"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_symptomId_fkey" FOREIGN KEY ("symptomId") REFERENCES "Symptom"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_preventionId_fkey" FOREIGN KEY ("preventionId") REFERENCES "Prevention"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_firstAidId_fkey" FOREIGN KEY ("firstAidId") REFERENCES "FirstAid"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_mythVsFactId_fkey" FOREIGN KEY ("mythVsFactId") REFERENCES "MythVsFact"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_doctorSignId_fkey" FOREIGN KEY ("doctorSignId") REFERENCES "DoctorSign"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,201 @@
-- CreateEnum
CREATE TYPE "StatusLaporan" AS ENUM ('SELESAI', 'PROSES', 'GAGAL');
-- AlterTable
ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_pkey" PRIMARY KEY ("id");
-- DropIndex
DROP INDEX "DataKematian_Kelahiran_id_key";
-- AlterTable
ALTER TABLE "GrafikKepuasan" ADD CONSTRAINT "GrafikKepuasan_pkey" PRIMARY KEY ("id");
-- DropIndex
DROP INDEX "GrafikKepuasan_id_key";
-- CreateTable
CREATE TABLE "KeamananLingkungan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"deskripsi" 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 "KeamananLingkungan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PolsekTerdekat" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"jarakKeDesa" TEXT NOT NULL,
"alamat" TEXT NOT NULL,
"nomorTelepon" TEXT NOT NULL,
"jamOperasional" TEXT NOT NULL,
"embedMapUrl" TEXT NOT NULL,
"namaTempatMaps" TEXT NOT NULL,
"alamatMaps" TEXT NOT NULL,
"linkPetunjukArah" TEXT NOT NULL,
"layananPolsekId" 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 "PolsekTerdekat_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LayananPolsek" (
"id" TEXT NOT NULL,
"nama" 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 "LayananPolsek_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KontakDaruratKeamanan" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"kontak" TEXT NOT NULL,
"icon" 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 "KontakDaruratKeamanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PencegahanKriminalitas" (
"id" TEXT NOT NULL,
"programKeamananId" TEXT NOT NULL,
"tipsKeamananId" TEXT NOT NULL,
"videoKeamananId" 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 "PencegahanKriminalitas_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProgramKeamanan" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"deskripsi" TEXT,
"slug" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProgramKeamanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TipsKeamanan" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"konten" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TipsKeamanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VideoKeamanan" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" TEXT,
"videoUrl" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "VideoKeamanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LaporanPublik" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"lokasi" TEXT NOT NULL,
"tanggalWaktu" TIMESTAMP(3) NOT NULL,
"status" "StatusLaporan" NOT NULL,
"kronologi" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "LaporanPublik_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PenangananLaporanPublik" (
"id" TEXT NOT NULL,
"laporanId" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
CONSTRAINT "PenangananLaporanPublik_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Pelapor" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"alamat" TEXT NOT NULL,
"nomorTelepon" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
CONSTRAINT "Pelapor_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MenuTipsKeamanan" (
"id" TEXT NOT NULL,
"judul" TEXT NOT NULL,
"deskripsi" 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 "MenuTipsKeamanan_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ProgramKeamanan_slug_key" ON "ProgramKeamanan"("slug");
-- AddForeignKey
ALTER TABLE "KeamananLingkungan" ADD CONSTRAINT "KeamananLingkungan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PolsekTerdekat" ADD CONSTRAINT "PolsekTerdekat_layananPolsekId_fkey" FOREIGN KEY ("layananPolsekId") REFERENCES "LayananPolsek"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PencegahanKriminalitas" ADD CONSTRAINT "PencegahanKriminalitas_programKeamananId_fkey" FOREIGN KEY ("programKeamananId") REFERENCES "ProgramKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PencegahanKriminalitas" ADD CONSTRAINT "PencegahanKriminalitas_tipsKeamananId_fkey" FOREIGN KEY ("tipsKeamananId") REFERENCES "TipsKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PencegahanKriminalitas" ADD CONSTRAINT "PencegahanKriminalitas_videoKeamananId_fkey" FOREIGN KEY ("videoKeamananId") REFERENCES "VideoKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PenangananLaporanPublik" ADD CONSTRAINT "PenangananLaporanPublik_laporanId_fkey" FOREIGN KEY ("laporanId") REFERENCES "LaporanPublik"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Pelapor" ADD CONSTRAINT "Pelapor_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MenuTipsKeamanan" ADD CONSTRAINT "MenuTipsKeamanan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "LayananPolsek" ALTER COLUMN "deletedAt" DROP NOT NULL,
ALTER COLUMN "deletedAt" DROP DEFAULT;

View File

@@ -0,0 +1,75 @@
/*
Warnings:
- You are about to drop the column `deletedAt` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `deskripsi` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `imageId` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `isActive` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `name` on the `KontakDarurat` table. All the data in the column will be lost.
- Added the required column `nama` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "KontakDarurat" DROP CONSTRAINT "KontakDarurat_imageId_fkey";
-- AlterTable
ALTER TABLE "KontakDarurat" DROP COLUMN "deletedAt",
DROP COLUMN "deskripsi",
DROP COLUMN "imageId",
DROP COLUMN "isActive",
DROP COLUMN "name",
ADD COLUMN "icon" TEXT,
ADD COLUMN "nama" TEXT NOT NULL,
ADD COLUMN "urutan" INTEGER;
-- CreateTable
CREATE TABLE "KontakItem" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"nomorTelepon" TEXT NOT NULL,
"icon" TEXT,
"kategoriId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "KontakItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PasarDesa" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"harga" INTEGER NOT NULL,
"satuan" TEXT NOT NULL,
"alamat" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"rating" DOUBLE PRECISION 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,
"kategoriId" TEXT NOT NULL,
CONSTRAINT "PasarDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KategoriMakanan" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "KategoriMakanan_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "KontakItem" ADD CONSTRAINT "KontakItem_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakDarurat"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriMakanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,38 @@
/*
Warnings:
- You are about to drop the column `icon` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `nama` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `urutan` on the `KontakDarurat` table. All the data in the column will be lost.
- You are about to drop the column `deletedAt` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
- You are about to drop the column `isActive` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
- You are about to drop the column `kontak` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
- Added the required column `deskripsi` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
- Added the required column `imageId` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
- Added the required column `name` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_kategoriId_fkey";
-- AlterTable
ALTER TABLE "KontakDarurat" DROP COLUMN "icon",
DROP COLUMN "nama",
DROP COLUMN "urutan",
ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "deskripsi" TEXT NOT NULL,
ADD COLUMN "imageId" TEXT NOT NULL,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "name" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "deletedAt",
DROP COLUMN "isActive",
DROP COLUMN "kontak",
ADD COLUMN "urutan" INTEGER;
-- AddForeignKey
ALTER TABLE "KontakDarurat" ADD CONSTRAINT "KontakDarurat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KontakItem" ADD CONSTRAINT "KontakItem_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakDaruratKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,85 @@
/*
Warnings:
- The values [SELESAI,PROSES,GAGAL] on the enum `StatusLaporan` will be removed. If these variants are still used in the database, this will fail.
- You are about to drop the column `icon` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
- You are about to drop the column `urutan` on the `KontakDaruratKeamanan` table. All the data in the column will be lost.
- You are about to drop the column `icon` on the `KontakItem` table. All the data in the column will be lost.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "StatusLaporan_new" AS ENUM ('Selesai', 'Proses', 'Gagal');
ALTER TABLE "LaporanPublik" ALTER COLUMN "status" TYPE "StatusLaporan_new" USING ("status"::text::"StatusLaporan_new");
ALTER TYPE "StatusLaporan" RENAME TO "StatusLaporan_old";
ALTER TYPE "StatusLaporan_new" RENAME TO "StatusLaporan";
DROP TYPE "StatusLaporan_old";
COMMIT;
-- AlterTable
ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "icon",
DROP COLUMN "urutan",
ADD COLUMN "imageId" TEXT;
-- AlterTable
ALTER TABLE "KontakItem" DROP COLUMN "icon",
ADD COLUMN "imageId" TEXT;
-- CreateTable
CREATE TABLE "LowonganPekerjaan" (
"id" TEXT NOT NULL,
"posisi" TEXT NOT NULL,
"namaPerusahaan" TEXT NOT NULL,
"lokasi" TEXT NOT NULL,
"tipePekerjaan" TEXT NOT NULL,
"gaji" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"kualifikasi" TEXT NOT NULL,
"tanggalPosting" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "LowonganPekerjaan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProgramKemiskinan" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"deskripsi" TEXT NOT NULL,
"ikonUrl" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"statistikId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProgramKemiskinan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StatistikKemiskinan" (
"id" TEXT NOT NULL,
"tahun" INTEGER NOT NULL,
"jumlah" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "StatistikKemiskinan_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ProgramKemiskinan_statistikId_key" ON "ProgramKemiskinan"("statistikId");
-- CreateIndex
CREATE UNIQUE INDEX "StatistikKemiskinan_tahun_key" ON "StatistikKemiskinan"("tahun");
-- AddForeignKey
ALTER TABLE "KontakDaruratKeamanan" ADD CONSTRAINT "KontakDaruratKeamanan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KontakItem" ADD CONSTRAINT "KontakItem_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProgramKemiskinan" ADD CONSTRAINT "ProgramKemiskinan_statistikId_fkey" FOREIGN KEY ("statistikId") REFERENCES "StatistikKemiskinan"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,59 @@
/*
Warnings:
- You are about to drop the column `alamat` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `deletedAt` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `isActive` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `kategoriId` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the column `satuan` on the `PasarDesa` table. All the data in the column will be lost.
- You are about to drop the `KategoriMakanan` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `alamatUsaha` to the `PasarDesa` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_imageId_fkey";
-- DropForeignKey
ALTER TABLE "PasarDesa" DROP CONSTRAINT "PasarDesa_kategoriId_fkey";
-- AlterTable
ALTER TABLE "PasarDesa" DROP COLUMN "alamat",
DROP COLUMN "deletedAt",
DROP COLUMN "isActive",
DROP COLUMN "kategoriId",
DROP COLUMN "satuan",
ADD COLUMN "alamatUsaha" TEXT NOT NULL,
ALTER COLUMN "imageId" DROP NOT NULL;
-- DropTable
DROP TABLE "KategoriMakanan";
-- CreateTable
CREATE TABLE "KategoriProduk" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "KategoriProduk_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_ProdukToKategori" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ProdukToKategori_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_ProdukToKategori_B_index" ON "_ProdukToKategori"("B");
-- AddForeignKey
ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ProdukToKategori" ADD CONSTRAINT "_ProdukToKategori_A_fkey" FOREIGN KEY ("A") REFERENCES "KategoriProduk"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ProdukToKategori" ADD CONSTRAINT "_ProdukToKategori_B_fkey" FOREIGN KEY ("B") REFERENCES "PasarDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,41 @@
/*
Warnings:
- You are about to drop the `_ProdukToKategori` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "_ProdukToKategori" DROP CONSTRAINT "_ProdukToKategori_A_fkey";
-- DropForeignKey
ALTER TABLE "_ProdukToKategori" DROP CONSTRAINT "_ProdukToKategori_B_fkey";
-- AlterTable
ALTER TABLE "KategoriProduk" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "PasarDesa" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
-- DropTable
DROP TABLE "_ProdukToKategori";
-- CreateTable
CREATE TABLE "KategoriToPasar" (
"id" TEXT NOT NULL,
"kategoriId" TEXT NOT NULL,
"pasarDesaId" 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 "KategoriToPasar_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "KategoriToPasar" ADD CONSTRAINT "KategoriToPasar_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriProduk"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KategoriToPasar" ADD CONSTRAINT "KategoriToPasar_pasarDesaId_fkey" FOREIGN KEY ("pasarDesaId") REFERENCES "PasarDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -73,9 +73,21 @@ model FileStorage {
Puskesmas Puskesmas[]
ProgramKesehatan ProgramKesehatan[]
PenangananDarurat PenangananDarurat[]
KontakDarurat KontakDarurat[]
KontakDarurat KontakDarurat[]
InfoWabahPenyakit InfoWabahPenyakit[]
KeamananLingkungan KeamananLingkungan[]
MenuTipsKeamanan MenuTipsKeamanan[]
Pelapor Pelapor[]
PasarDesa PasarDesa[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
KontakItem KontakItem[]
}
//========================================= MENU PPID ========================================= //
@@ -471,24 +483,24 @@ model Penghargaan {
// ========================================= FASILITAS KESEHATAN ========================================= //
model FasilitasKesehatan {
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
informasiumum InformasiUmum @relation(fields: [informasiUmumId], references: [id])
informasiUmumId String
layananunggulan LayananUnggulan @relation(fields: [layananUnggulanId], references: [id])
layananUnggulanId String
dokterdantenagamedis DokterdanTenagaMedis @relation(fields: [dokterdanTenagaMedisId], references: [id])
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
informasiumum InformasiUmum @relation(fields: [informasiUmumId], references: [id])
informasiUmumId String
layananunggulan LayananUnggulan @relation(fields: [layananUnggulanId], references: [id])
layananUnggulanId String
dokterdantenagamedis DokterdanTenagaMedis @relation(fields: [dokterdanTenagaMedisId], references: [id])
dokterdanTenagaMedisId String
fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id])
fasilitasPendukungId String
prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id])
prosedurPendaftaranId String
tarifdanlayanan TarifDanLayanan @relation(fields: [tarifDanLayananId], references: [id])
tarifDanLayananId String
fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id])
fasilitasPendukungId String
prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id])
prosedurPendaftaranId String
tarifdanlayanan TarifDanLayanan @relation(fields: [tarifDanLayananId], references: [id])
tarifDanLayananId String
}
model InformasiUmum {
@@ -558,46 +570,46 @@ model TarifDanLayanan {
// ========================================= JADWAL KEGIATAN ========================================= //
model JadwalKegiatan {
id String @id @default(cuid())
content String
informasijadwalkegiatan InformasiJadwalKegiatan @relation(fields: [informasiJadwalKegiatanId], references: [id])
informasiJadwalKegiatanId String
deskripsijadwalkegiatan DeskripsiJadwalKegiatan @relation(fields: [deskripsiJadwalKegiatanId], references: [id])
deskripsiJadwalKegiatanId String
layananjadwalkegiatan LayananJadwalKegiatan @relation(fields: [layananJadwalKegiatanId], references: [id])
layananJadwalKegiatanId String
syaratketentuanjadwalkegiatan SyaratKetentuanJadwalKegiatan @relation(fields: [syaratKetentuanJadwalKegiatanId], references: [id])
id String @id @default(cuid())
content String
informasijadwalkegiatan InformasiJadwalKegiatan @relation(fields: [informasiJadwalKegiatanId], references: [id])
informasiJadwalKegiatanId String
deskripsijadwalkegiatan DeskripsiJadwalKegiatan @relation(fields: [deskripsiJadwalKegiatanId], references: [id])
deskripsiJadwalKegiatanId String
layananjadwalkegiatan LayananJadwalKegiatan @relation(fields: [layananJadwalKegiatanId], references: [id])
layananJadwalKegiatanId String
syaratketentuanjadwalkegiatan SyaratKetentuanJadwalKegiatan @relation(fields: [syaratKetentuanJadwalKegiatanId], references: [id])
syaratKetentuanJadwalKegiatanId String
dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
dokumenJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
dokumenJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model InformasiJadwalKegiatan {
id String @id @default(cuid())
name String
tanggal String
waktu String
lokasi String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
name String
tanggal String
waktu String
lokasi String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
JadwalKegiatan JadwalKegiatan[]
}
model DeskripsiJadwalKegiatan {
id String @id @default(cuid())
deskripsi String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
deskripsi String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
JadwalKegiatan JadwalKegiatan[]
}
@@ -624,34 +636,33 @@ model SyaratKetentuanJadwalKegiatan {
}
model DokumenJadwalKegiatan {
id String @id @default(cuid())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
JadwalKegiatan JadwalKegiatan[]
}
model PendaftaranJadwalKegiatan {
id String @id @default(cuid())
name String
tanggal String
namaOrangtua String
nomor String
alamat String
catatan String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
name String
tanggal String
namaOrangtua String
nomor String
alamat String
catatan String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
JadwalKegiatan JadwalKegiatan[]
}
// ========================================= PERSENTASE KELAHIRAN & KEMATIAN ========================================= //
model DataKematian_Kelahiran {
id Int @id @default(autoincrement())
uuid String @default(cuid()) @unique
id String @id @default(cuid())
tahun String
kematianKasar String
kematianBayi String
@@ -664,8 +675,7 @@ model DataKematian_Kelahiran {
// ========================================= GRAFIK KEPUASAN ========================================= //
model GrafikKepuasan {
id Int @id @default(autoincrement())
uuid String @default(cuid()) @unique
id String @id @default(cuid())
label String
jumlah String
createdAt DateTime @default(now())
@@ -676,56 +686,74 @@ model GrafikKepuasan {
// ========================================= ARTIKEL KESEHATAN ========================================= //
model ArtikelKesehatan {
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
title String
content String
introduction Introduction @relation(fields: [introductionId], references: [id])
introductionId String
symptom Symptom @relation(fields: [symptomId], references: [id])
symptomId String
prevention Prevention @relation(fields: [preventionId], references: [id])
preventionId String
firstaid FirstAid @relation(fields: [firstAidId], references: [id])
firstAidId String
mythvsfact MythVsFact @relation(fields: [mythVsFactId], references: [id])
mythVsFactId String
doctorsign DoctorSign @relation(fields: [doctorSignId], references: [id])
doctorSignId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model Introduction {
id String @id @default(cuid())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
ArtikelKesehatan ArtikelKesehatan[]
}
model Symptom {
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
ArtikelKesehatan ArtikelKesehatan[]
}
model Prevention {
id String @id @default(cuid())
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
ArtikelKesehatan ArtikelKesehatan[]
}
model FirstAid {
id String @id @default(cuid())
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
ArtikelKesehatan ArtikelKesehatan[]
}
model MythVsFact {
id String @id @default(cuid())
id String @id @default(cuid())
title String
mitos String
fakta String
@@ -733,15 +761,19 @@ model MythVsFact {
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
ArtikelKesehatan ArtikelKesehatan[]
}
model DoctorSign {
id String @id @default(cuid())
id String @id @default(cuid())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
ArtikelKesehatan ArtikelKesehatan[]
}
// ========================================= POSYANDU ========================================= //
@@ -839,16 +871,261 @@ model KontakDarurat {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= INFO WABAH PENYAKIT ========================================= //
model InfoWabahPenyakit {
id String @id @default(cuid())
name String
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)
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= MENU KEAMANAN ========================================= //
// ========================================= KEAMANAN LINGKUNGAN ========================================= //
model KeamananLingkungan {
id String @id @default(cuid())
name 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)
}
// ========================================= POLSEK TERDEKAT ========================================= //
model PolsekTerdekat {
id String @id @default(uuid())
nama String
jarakKeDesa String
alamat String
nomorTelepon String
jamOperasional String
embedMapUrl String
namaTempatMaps String
alamatMaps String
linkPetunjukArah String
layananPolsek LayananPolsek @relation(fields: [layananPolsekId], references: [id])
layananPolsekId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model LayananPolsek {
id String @id @default(uuid())
nama String // contoh: "Pelayanan SKCK", "Laporan Kriminal"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
PolsekTerdekat PolsekTerdekat[]
}
// ========================================= KONTAK DARURAT ========================================= //
model KontakDaruratKeamanan {
id String @id @default(uuid())
nama String // contoh: "Layanan Darurat", "Fasilitas Kesehatan"
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
kontakItems KontakItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model KontakItem {
id String @id @default(uuid())
nama String // contoh: "Polisi", "Ambulans", "Puskesmas Darmasaba"
nomorTelepon String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
kategori KontakDaruratKeamanan @relation(fields: [kategoriId], references: [id])
kategoriId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
model PencegahanKriminalitas {
id String @id @default(cuid())
programKeamanan ProgramKeamanan @relation(fields: [programKeamananId], references: [id])
programKeamananId String
tipsKeamanan TipsKeamanan @relation(fields: [tipsKeamananId], references: [id])
tipsKeamananId String
videoKeamanan VideoKeamanan @relation(fields: [videoKeamananId], references: [id])
videoKeamananId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model ProgramKeamanan {
id String @id @default(cuid())
nama String // contoh: "Ronda Malam"
deskripsi String? // jika mau tambahkan info detail
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
model TipsKeamanan {
id String @id @default(cuid())
judul String
konten String
slug String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
model VideoKeamanan {
id String @id @default(cuid())
judul String
deskripsi String?
videoUrl String // link youtube atau embed url
slug String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
PencegahanKriminalitas PencegahanKriminalitas[]
}
// ========================================= LAPORAN PUBLIK ========================================= //
model LaporanPublik {
id String @id @default(cuid())
judul String
lokasi String
tanggalWaktu DateTime
status StatusLaporan
penanganan PenangananLaporanPublik[]
kronologi String? // Optional, bisa diisi detail kronologi
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model PenangananLaporanPublik {
id String @id @default(cuid())
laporanId String
deskripsi String
laporan LaporanPublik @relation(fields: [laporanId], references: [id], onDelete: Cascade)
}
enum StatusLaporan {
Selesai
Proses
Gagal
}
model Pelapor {
id String @id @default(cuid())
nama String
alamat String
nomorTelepon String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
}
// ========================================= TIPS KEAMANAN ========================================= //
model MenuTipsKeamanan {
id String @id @default(cuid())
judul 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)
}
// ========================================= MENU EKONOMI ========================================= //
// ========================================= PASAR DESA ========================================= //
model PasarDesa {
id String @id @default(uuid())
nama String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
harga Int
rating Float
alamatUsaha String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
kategoriProduk KategoriProduk @relation(fields: [kategoriProdukId], references: [id])
kategoriProdukId String
KategoriToPasar KategoriToPasar[]
}
model KategoriProduk {
id String @id @default(uuid())
nama String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
KategoriToPasar KategoriToPasar[]
PasarDesa PasarDesa[]
}
model KategoriToPasar {
id String @id @default(uuid())
kategori KategoriProduk @relation(fields: [kategoriId], references: [id])
kategoriId String
pasarDesa PasarDesa @relation(fields: [pasarDesaId], references: [id])
pasarDesaId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= LOWONGAN KERJA LOKAL ========================================= //
model LowonganPekerjaan {
id String @id @default(uuid()) // ID unik untuk setiap lowongan
posisi String // Contoh: "Kasir"
namaPerusahaan String // Contoh: "Toko Sumber Rejeki"
lokasi String // Contoh: "Desa Munggu , Badung"
tipePekerjaan String // Contoh: "Full Time", "Part Time", "Contract"
gaji String // Contoh: "Rp. 2.500.000 / bulan". Menggunakan String karena formatnya bisa bervariasi
deskripsi String // Opsional: Detail deskripsi pekerjaan (tidak terlihat di UI ini, tapi umum ada)
kualifikasi String // Opsional: Kualifikasi yang dibutuhkan (tidak terlihat di UI ini, tapi umum ada)
tanggalPosting DateTime @default(now()) // Tanggal lowongan diposting
isActive Boolean @default(true) // Menandakan apakah lowongan masih aktif
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
}
// ========================================= PROGRAM KEMISKINAN ========================================= //
model ProgramKemiskinan {
id String @id @default(uuid())
nama String
deskripsi String
ikonUrl String?
isActive Boolean @default(true)
// Tambahkan relasi one-to-one ke StatistikKemiskinan
statistikId String? @unique // Foreign key ke StatistikKemiskinan, unique untuk one-to-one
statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model StatistikKemiskinan {
id String @id @default(uuid())
tahun Int @unique
jumlah Int
// Tidak perlu foreign key di sini jika relasi di ProgramLayanan
programKemiskinan ProgramKemiskinan?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

View File

@@ -17,6 +17,7 @@ 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";
import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
(async () => {
for (const l of layanan) {
@@ -340,6 +341,22 @@ import profilPerbekel from "./data/desa/profile/profil_perbekel.json";
});
}
console.log("dasar hukum PPID success ...");
for (const k of kategoriProduk) {
await prisma.kategoriProduk.upsert({
where: {
id: k.id,
},
update: {
nama: k.nama,
},
create: {
id: k.id,
nama: k.nama,
},
});
}
console.log("kategori produk success ...");
})()
.then(() => prisma.$disconnect())
.catch((e) => {

View File

@@ -0,0 +1,225 @@
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({
posisi: z.string(),
namaPerusahaan: z.string(),
lokasi: z.string(),
tipePekerjaan: z.string(),
gaji: z.string(),
deskripsi: z.string(),
kualifikasi: z.string(),
});
const defaultForm = {
posisi: "",
namaPerusahaan: "",
lokasi: "",
tipePekerjaan: "",
gaji: "",
deskripsi: "",
kualifikasi: "",
};
const lowonganKerjaState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(lowonganKerjaState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
lowonganKerjaState.create.loading = true;
const res = await ApiFetch.api.ekonomi.lowongankerja["create"].post(
lowonganKerjaState.create.form
);
if (res.status === 200) {
lowonganKerjaState.create.loading = false;
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
lowonganKerjaState.create.loading = false;
}
},
resetForm() {
lowonganKerjaState.create.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.LowonganPekerjaanGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.ekonomi.lowongankerja["find-many"].get();
if (res.status === 200) {
lowonganKerjaState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.LowonganPekerjaanGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/lowongankerja/${id}`);
if (res.ok) {
const data = await res.json();
lowonganKerjaState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
lowonganKerjaState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
lowonganKerjaState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
lowonganKerjaState.delete.loading = true;
const response = await fetch(`/api/ekonomi/lowongankerja/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Lowongan kerja berhasil dihapus");
await lowonganKerjaState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus lowongan kerja");
}
} catch (error) {
console.error("Error deleting data:", error);
toast.error("Terjadi kesalahan saat menghapus lowongan kerja");
} finally {
lowonganKerjaState.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/lowongankerja/${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 = {
posisi: data.posisi,
namaPerusahaan: data.namaPerusahaan,
lokasi: data.lokasi,
tipePekerjaan: data.tipePekerjaan,
gaji: data.gaji,
deskripsi: data.deskripsi,
kualifikasi: data.kualifikasi,
};
return data;
} else {
throw new Error(
result?.message || "Gagal memuat data lowongan kerja"
);
}
} catch (error) {
console.error("Error fetching data:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(lowonganKerjaState.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
lowonganKerjaState.update.loading = true;
const response = await fetch(`/api/ekonomi/lowongankerja/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
posisi: this.form.posisi,
namaPerusahaan: this.form.namaPerusahaan,
lokasi: this.form.lokasi,
tipePekerjaan: this.form.tipePekerjaan,
gaji: this.form.gaji,
deskripsi: this.form.deskripsi,
kualifikasi: this.form.kualifikasi,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update lowongan kerja");
await lowonganKerjaState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update lowongan kerja");
}
} catch (error) {
console.error("Error updating data:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate lowongan kerja");
return false;
} finally {
lowonganKerjaState.update.loading = false;
}
},
reset() {
lowonganKerjaState.update.id = "";
lowonganKerjaState.update.form = { ...defaultForm };
},
},
});
export default lowonganKerjaState;

View File

@@ -0,0 +1,459 @@
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 templatePasarDesaForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
harga: z.number().min(1, "Harga minimal 1"),
alamatUsaha: z.string().min(1, "Alamat minimal 1 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
rating: z.number().min(1, "Rating minimal 1"),
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
});
const defaultPasarDesaForm = {
nama: "",
harga: 0,
alamatUsaha: "",
imageId: "",
rating: 0,
kategoriId: [] as string[],
};
const pasarDesa = proxy({
create: {
form: { ...defaultPasarDesaForm },
loading: false,
async create() {
const cek = templatePasarDesaForm.safeParse(pasarDesa.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pasarDesa.create.loading = true;
const res = await ApiFetch.api.ekonomi.pasardesa["create"].post(
pasarDesa.create.form
);
if (res.status === 200) {
pasarDesa.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
pasarDesa.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.PasarDesaGetPayload<{
include: {
image: true;
KategoriToPasar: {
include: {
kategori: true;
};
};
};
}>
> | null,
async load() {
const res = await ApiFetch.api.ekonomi.pasardesa["find-many"].get();
if (res.status === 200) {
pasarDesa.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PasarDesaGetPayload<{
include: {
image: true;
KategoriToPasar: {
include: {
kategori: true;
};
};
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/pasardesa/${id}`);
if (res.ok) {
const data = await res.json();
pasarDesa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pasarDesa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pasarDesa.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pasarDesa.delete.loading = true;
const response = await fetch(`/api/ekonomi/pasardesa/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Pasar desa berhasil dihapus");
await pasarDesa.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pasar desa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pasar desa");
} finally {
pasarDesa.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultPasarDesaForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/pasardesa/${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 = {
nama: data.nama,
harga: data.harga,
alamatUsaha: data.alamatUsaha,
imageId: data.imageId,
rating: data.rating,
kategoriId: data.kategoriId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pasar desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templatePasarDesaForm.safeParse(pasarDesa.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pasarDesa.edit.loading = true;
const response = await fetch(`/api/ekonomi/pasardesa/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
harga: this.form.harga,
alamatUsaha: this.form.alamatUsaha,
imageId: this.form.imageId,
rating: this.form.rating,
kategoriId: this.form.kategoriId,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pasar desa");
await pasarDesa.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate pasar desa");
}
} catch (error) {
console.error("Error updating pasar desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate pasar desa"
);
return false;
} finally {
pasarDesa.edit.loading = false;
}
},
reset() {
pasarDesa.edit.id = "";
pasarDesa.edit.form = { ...defaultPasarDesaForm };
},
},
});
// ========================================= KATEGORI PRODUK ========================================= //
const kategoriProdukForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
});
const kategoriProdukDefaultForm = {
nama: "",
};
const kategoriProduk = proxy({
create: {
form: { ...kategoriProdukDefaultForm },
loading: false,
async create() {
const cek = kategoriProdukForm.safeParse(kategoriProduk.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kategoriProduk.create.loading = true;
const res = await ApiFetch.api.ekonomi.kategoriproduk["create"].post(
kategoriProduk.create.form
);
if (res.status === 200) {
kategoriProduk.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
kategoriProduk.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
}>
| null,
async load() {
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many"].get();
if (res.status === 200) {
kategoriProduk.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KategoriProdukGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/kategoriproduk/${id}`);
if (res.ok) {
const data = await res.json();
kategoriProduk.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kategoriProduk.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kategoriProduk.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kategoriProduk.delete.loading = true;
const response = await fetch(`/api/ekonomi/kategoriproduk/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kategori produk berhasil dihapus");
await kategoriProduk.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kategori produk");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kategori produk");
} finally {
kategoriProduk.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...kategoriProdukDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/kategoriproduk/${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 = {
nama: data.nama,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kategori produk:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = kategoriProdukForm.safeParse(kategoriProduk.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
kategoriProduk.edit.loading = true;
const response = await fetch(
`/api/ekonomi/kategoriproduk/${kategoriProduk.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: kategoriProduk.edit.form.nama,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error('Update failed with status:', response.status, 'Response:', result);
throw new Error(
result?.message || `Gagal mengupdate kategori produk (${response.status})`
);
}
if (result.success) {
toast.success(result.message || "Berhasil memperbarui kategori produk");
await kategoriProduk.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate kategori produk");
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error('Error response text:', text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error('Error parsing response as text:', textError);
console.error('Original error:', error);
throw new Error('Gagal memproses respons dari server');
}
}
} catch (error) {
console.error("Error updating kategori produk:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kategori produk"
);
return false;
} finally {
kategoriProduk.edit.loading = false;
}
},
reset() {
kategoriProduk.edit.id = "";
kategoriProduk.edit.form = { ...kategoriProdukDefaultForm };
},
},
});
const pasarDesaState = proxy({
pasarDesa,
kategoriProduk
});
export default pasarDesaState;

View File

@@ -0,0 +1,249 @@
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({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
ikonUrl: z.string().optional(),
statistik: z.object({
tahun: z.string().min(1, "Tahun minimal 1 karakter"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
})
});
const defaultForm = {
nama: "",
deskripsi: "",
ikonUrl: "",
statistik: {
tahun: "",
jumlah: ""
}
};
const programKemiskinanState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(programKemiskinanState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programKemiskinanState.create.loading = true;
const res = await ApiFetch.api.ekonomi.programkemiskinan["create"].post(
programKemiskinanState.create.form
);
if (res.status === 200) {
programKemiskinanState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
programKemiskinanState.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.ProgramKemiskinanGetPayload<{
include: {
statistik: true;
};
}>[],
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.programkemiskinan[
"find-many"
].get();
if (res.status === 200) {
programKemiskinanState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.ProgramKemiskinanGetPayload<{
include: {
statistik: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/programkemiskinan/${id}`);
if (res.ok) {
const data = await res.json();
programKemiskinanState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
programKemiskinanState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
programKemiskinanState.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/programkemiskinan/${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 = {
nama: data.nama,
deskripsi: data.deskripsi,
ikonUrl: data.ikonUrl || "",
statistik: {
tahun: data.statistik.tahun,
jumlah: data.statistik.jumlah,
},
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading program kemiskinan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(programKemiskinanState.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
programKemiskinanState.update.loading = true;
const response = await fetch(
`/api/ekonomi/programkemiskinan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
deskripsi: this.form.deskripsi,
ikonUrl: this.form.ikonUrl,
statistik: {
tahun: this.form.statistik.tahun,
jumlah: this.form.statistik.jumlah,
},
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update program kemiskinan");
await programKemiskinanState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update program kemiskinan");
}
} catch (error) {
console.error("Error updating program kemiskinan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update program kemiskinan"
);
return false;
} finally {
programKemiskinanState.update.loading = false;
}
},
reset() {
programKemiskinanState.update.id = "";
programKemiskinanState.update.form = { ...defaultForm };
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
programKemiskinanState.delete.loading = true;
const response = await fetch(
`/api/ekonomi/programkemiskinan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Program kemiskinan berhasil dihapus"
);
await programKemiskinanState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus program kemiskinan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus program kemiskinan");
} finally {
programKemiskinanState.delete.loading = false;
}
},
},
});
export default programKemiskinanState;

View File

@@ -0,0 +1,232 @@
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, "Nama minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(),
});
const defaultForm = {
name: "",
deskripsi: "",
imageId: "",
};
const keamananLingkunganState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(keamananLingkunganState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
keamananLingkunganState.create.loading = true;
const res = await ApiFetch.api.keamanan.keamananlingkungan[
"create"
].post(keamananLingkunganState.create.form);
if (res.status === 200) {
keamananLingkunganState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
keamananLingkunganState.create.loading = false;
}
},
resetForm() {
keamananLingkunganState.create.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.KeamananLingkunganGetPayload<{
include: { image: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.keamananlingkungan[
"find-many"
].get();
if (res.status === 200) {
keamananLingkunganState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KeamananLingkunganGetPayload<{
include: { image: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/keamananlingkungan/${id}`);
if (res.ok) {
const data = await res.json();
keamananLingkunganState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
keamananLingkunganState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
keamananLingkunganState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
keamananLingkunganState.delete.loading = true;
const response = await fetch(
`/api/keamanan/keamananlingkungan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Keamanan ingkungan berhasil dihapus"
);
await keamananLingkunganState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus keamanan ingkungan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus keamanan ingkungan");
} finally {
keamananLingkunganState.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/keamanan/keamananlingkungan/${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 loading keamanan lingkungan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(keamananLingkunganState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
keamananLingkunganState.edit.loading = true;
const response = await fetch(
`/api/keamanan/keamananlingkungan/${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("Berhasil update keamanan lingkungan");
await keamananLingkunganState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update keamanan lingkungan");
}
} catch (error) {
console.error("Error updating keamanan lingkungan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update keamanan lingkungan"
);
return false;
} finally {
keamananLingkunganState.edit.loading = false;
}
},
reset() {
keamananLingkunganState.edit.id = "";
keamananLingkunganState.edit.form = { ...defaultForm };
},
},
});
export default keamananLingkunganState;

View File

@@ -0,0 +1,259 @@
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({
nama: z.string().min(1, "Nama minimal 1 karakter"),
imageId: z.string().nonempty(),
kontakItems: z.array(
z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
imageId: z.string().nonempty(),
})
),
});
const defaultForm = {
nama: "",
imageId: "",
kontakItems: [
{
nama: "",
nomorTelepon: "",
imageId: "",
},
],
};
const kontakDaruratKeamananState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(
kontakDaruratKeamananState.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDaruratKeamananState.create.loading = true;
const res = await ApiFetch.api.keamanan.kontakdaruratkeamanan[
"create"
].post(kontakDaruratKeamananState.create.form);
if (res.status === 200) {
kontakDaruratKeamananState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
kontakDaruratKeamananState.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.KontakDaruratKeamananGetPayload<{
include: {
kontakItems: true;
image: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.kontakdaruratkeamanan[
"find-many"
].get();
if (res.status === 200) {
kontakDaruratKeamananState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KontakDaruratKeamananGetPayload<{
include: {
kontakItems: {
include: {
image: true;
};
};
image: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/kontakdaruratkeamanan/${id}`);
if (res.ok) {
const data = await res.json();
kontakDaruratKeamananState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kontakDaruratKeamananState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
kontakDaruratKeamananState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kontakDaruratKeamananState.delete.loading = true;
const response = await fetch(
`/api/keamanan/kontakdaruratkeamanan/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 kontakDaruratKeamananState.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 {
kontakDaruratKeamananState.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/keamanan/kontakdaruratkeamanan/${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 = {
nama: data.nama,
imageId: data.imageId,
kontakItems: [
{
nama: data.kontakItems.nama,
nomorTelepon: data.kontakItems.nomorTelepon,
imageId: data.kontakItems.imageId,
},
],
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading kontak darurat:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(
kontakDaruratKeamananState.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kontakDaruratKeamananState.update.loading = true;
const response = await fetch(
`/api/keamanan/kontakdaruratkeamanan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
imageId: this.form.imageId,
kontakItems: [
{
nama: this.form.kontakItems[0].nama,
nomorTelepon: this.form.kontakItems[0].nomorTelepon,
imageId: this.form.kontakItems[0].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("Berhasil update kontak darurat");
await kontakDaruratKeamananState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate kontak darurat");
}
} catch (error) {
console.error("Error updating kontak darurat:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate kontak darurat"
);
return false;
} finally {
kontakDaruratKeamananState.update.loading = false;
}
},
reset() {
kontakDaruratKeamananState.update.id = "";
kontakDaruratKeamananState.update.form = { ...defaultForm };
},
},
});
export default kontakDaruratKeamananState;

View File

@@ -0,0 +1,273 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
export type Status = "Selesai" | "Proses" | "Gagal";
const templateForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
lokasi: z.string().min(3, "Lokasi minimal 3 karakter"),
tanggalWaktu: z.string().min(3, "Tanggal Waktu minimal 3 karakter"),
status: z.enum(["Selesai", "Proses", "Gagal"]),
penanganan: z.string(),
kronologi: z.string().optional(),
});
interface FormData {
judul: string;
lokasi: string;
tanggalWaktu: string;
status: Status;
penanganan: string;
kronologi: string;
}
const defaultForm: FormData = {
judul: "",
lokasi: "",
tanggalWaktu: new Date().toISOString(),
status: "Proses",
penanganan: "",
kronologi: "",
};
const laporanPublikState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(laporanPublikState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
laporanPublikState.create.loading = true;
// Ensure we have a valid date
if (!laporanPublikState.create.form.tanggalWaktu) {
return toast.error("Tanggal laporan harus diisi");
}
// Format the data before sending
const formData = {
...laporanPublikState.create.form,
// Ensure the date is in the correct format for the API
tanggalWaktu: new Date(laporanPublikState.create.form.tanggalWaktu).toISOString()
};
console.log("Sending form data:", formData); // Debug log
const res = await ApiFetch.api.keamanan.laporanpublik["create"].post(
formData
);
if (res.error) {
console.error("API Error:", res.error);
throw new Error("Failed to create laporan publik");
}
if (res.status === 200) {
laporanPublikState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.error("Error creating laporan publik:", error);
toast.error(error instanceof Error ? error.message : "Gagal membuat laporan publik");
throw error; // Re-throw to be handled by the caller
} finally {
laporanPublikState.create.loading = false;
}
},
resetForm() {
laporanPublikState.create.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.LaporanPublikGetPayload<{
include: { penanganan: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.laporanpublik["find-many"].get();
if (res.status === 200) {
laporanPublikState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.LaporanPublikGetPayload<{
include: { penanganan: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/laporanpublik/${id}`);
if (res.ok) {
const data = await res.json();
laporanPublikState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
laporanPublikState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
laporanPublikState.findUnique.data = null;
}
},
resetForm() {
laporanPublikState.findUnique.data = null;
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
laporanPublikState.delete.loading = true;
const response = await fetch(`/api/keamanan/laporanpublik/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Laporan publik berhasil dihapus"
);
await laporanPublikState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus laporan publik");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus laporan publik");
} finally {
laporanPublikState.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/keamanan/laporanpublik/${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 = {
judul: data.judul,
lokasi: data.lokasi,
tanggalWaktu: data.tanggalWaktu,
status: data.status,
penanganan: data.penanganan,
kronologi: data.kronologi,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading keamanan lingkungan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(laporanPublikState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
laporanPublikState.edit.loading = true;
const response = await fetch(
`/api/keamanan/laporanpublik/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
judul: this.form.judul,
lokasi: this.form.lokasi,
tanggalWaktu: this.form.tanggalWaktu,
status: this.form.status,
penanganan: this.form.penanganan,
kronologi: this.form.kronologi,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update laporan publik");
await laporanPublikState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update laporan publik");
}
} catch (error) {
console.error("Error updating laporan publik:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update laporan publik"
);
return false;
} finally {
laporanPublikState.edit.loading = false;
}
},
reset() {
laporanPublikState.edit.id = "";
laporanPublikState.edit.form = { ...defaultForm };
},
}
});
export default laporanPublikState;

View File

@@ -0,0 +1,314 @@
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({
pencegahanKriminalitas: z.object({
programKeamanan: z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
tipsKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
konten: z.string().min(1, "Konten minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
videoKeamanan: z.object({
judul: z.string().min(1, "Judul minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
videoUrl: z.string().min(1, "Video URL minimal 1 karakter"),
slug: z.string().min(1, "Slug minimal 1 karakter"),
}),
}),
});
const defaultForm = {
pencegahanKriminalitas: {
programKeamanan: {
nama: "",
deskripsi: "",
slug: "",
},
tipsKeamanan: {
judul: "",
konten: "",
slug: "",
},
videoKeamanan: {
judul: "",
deskripsi: "",
videoUrl: "",
slug: "",
},
},
};
const pencegahanKriminalitasState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(
pencegahanKriminalitasState.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pencegahanKriminalitasState.create.loading = true;
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"create"
].post(pencegahanKriminalitasState.create.form.pencegahanKriminalitas);
if (res.status === 200) {
pencegahanKriminalitasState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
pencegahanKriminalitasState.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.PencegahanKriminalitasGetPayload<{
include: {
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.pencegahankriminalitas[
"find-many"
].get();
if (res.status === 200) {
pencegahanKriminalitasState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PencegahanKriminalitasGetPayload<{
include: {
programKeamanan: true;
tipsKeamanan: true;
videoKeamanan: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/pencegahankriminalitas/${id}`);
if (res.ok) {
const data = await res.json();
pencegahanKriminalitasState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pencegahanKriminalitasState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pencegahanKriminalitasState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pencegahanKriminalitasState.delete.loading = true;
const response = await fetch(
`/api/keamanan/pencegahankriminalitas/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Pencegahan kriminalitas berhasil dihapus"
);
await pencegahanKriminalitasState.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus pencegahan kriminalitas"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pencegahan kriminalitas");
} finally {
pencegahanKriminalitasState.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
pencegahanKriminalitasState.update.loading = true;
const response = await fetch(
`/api/keamanan/pencegahankriminalitas/${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;
pencegahanKriminalitasState.update.id = data.id;
pencegahanKriminalitasState.update.form = {
pencegahanKriminalitas: {
programKeamanan: {
nama: data.programKeamanan.nama,
deskripsi: data.programKeamanan.deskripsi,
slug: data.programKeamanan.slug,
},
tipsKeamanan: {
judul: data.tipsKeamanan.judul,
konten: data.tipsKeamanan.konten,
slug: data.tipsKeamanan.slug,
},
videoKeamanan: {
judul: data.videoKeamanan.judul,
deskripsi: data.videoKeamanan.deskripsi,
videoUrl: data.videoKeamanan.videoUrl,
slug: data.videoKeamanan.slug,
},
},
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(
"Terjadi kesalahan saat mengupdate pencegahan kriminalitas"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(
pencegahanKriminalitasState.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pencegahanKriminalitasState.update.loading = true;
const response = await fetch(
`/api/keamanan/pencegahankriminalitas/${pencegahanKriminalitasState.update.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
pencegahanKriminalitas: {
programKeamanan: {
nama: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.nama,
deskripsi:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.deskripsi,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.programKeamanan.slug,
},
tipsKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.judul,
konten:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.konten,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.tipsKeamanan.slug,
},
videoKeamanan: {
judul:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.judul,
deskripsi:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.deskripsi,
videoUrl:
pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.videoUrl,
slug: pencegahanKriminalitasState.update.form
.pencegahanKriminalitas.videoKeamanan.slug,
},
},
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pencegahan kriminalitas");
await pencegahanKriminalitasState.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate pencegahan kriminalitas"
);
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate pencegahan kriminalitas"
);
return false;
} finally {
pencegahanKriminalitasState.update.loading = false;
}
},
reset() {
pencegahanKriminalitasState.update.id = "";
pencegahanKriminalitasState.update.form = { ...defaultForm };
},
},
});
export default pencegahanKriminalitasState;

View File

@@ -0,0 +1,242 @@
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({
nama: z.string().min(1, "Nama minimal 1 karakter"),
jarakKeDesa: z.string().min(1, "Jarak minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor Telepon minimal 1 karakter"),
jamOperasional: z.string().min(1, "Jam Operasional minimal 1 karakter"),
embedMapUrl: z.string().min(1, "Embed Map Url minimal 1 karakter"),
namaTempatMaps: z.string().min(1, "Nama Tempat Maps minimal 1 karakter"),
alamatMaps: z.string().min(1, "Alamat Maps minimal 1 karakter"),
linkPetunjukArah: z.string().min(1, "Link Petunjuk Arah minimal 1 karakter"),
layananPolsekId: z.string().min(1, "Layanan Polsek Id minimal 1 karakter"),
});
const defaultForm = {
nama: "",
jarakKeDesa: "",
alamat: "",
nomorTelepon: "",
jamOperasional: "",
embedMapUrl: "",
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: "",
};
const polsekTerdekatState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(polsekTerdekatState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
polsekTerdekatState.create.loading = true;
const res = await ApiFetch.api.keamanan.polsekterdekat["create"].post(
polsekTerdekatState.create.form
);
if (res.status === 200) {
polsekTerdekatState.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
polsekTerdekatState.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.PolsekTerdekatGetPayload<{
include: { layananPolsek: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.keamanan.polsekterdekat["find-many"].get();
if (res.status === 200) {
polsekTerdekatState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.PolsekTerdekatGetPayload<{
include: { layananPolsek: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/polsekterdekat/${id}`);
if (res.ok) {
const data = await res.json();
polsekTerdekatState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
polsekTerdekatState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
polsekTerdekatState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
polsekTerdekatState.delete.loading = true;
const response = await fetch(`/api/keamanan/polsekterdekat/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Polsek terdekat berhasil dihapus");
await polsekTerdekatState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus polsek terdekat");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus polsek terdekat");
} finally {
polsekTerdekatState.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/keamanan/polsekterdekat/${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 = {
nama: data.nama,
jarakKeDesa: data.jarakKeDesa,
alamat: data.alamat,
nomorTelepon: data.nomorTelepon,
jamOperasional: data.jamOperasional,
embedMapUrl: data.embedMapUrl,
namaTempatMaps: data.namaTempatMaps,
alamatMaps: data.alamatMaps,
linkPetunjukArah: data.linkPetunjukArah,
layananPolsekId: data.layananPolsekId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading polsek terdekat:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(polsekTerdekatState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
polsekTerdekatState.edit.loading = true;
const response = await fetch(
`/api/keamanan/polsekterdekat/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
jarakKeDesa: this.form.jarakKeDesa,
alamat: this.form.alamat,
nomorTelepon: this.form.nomorTelepon,
jamOperasional: this.form.jamOperasional,
embedMapUrl: this.form.embedMapUrl,
namaTempatMaps: this.form.namaTempatMaps,
alamatMaps: this.form.alamatMaps,
linkPetunjukArah: this.form.linkPetunjukArah,
layananPolsekId: this.form.layananPolsekId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update polsek terdekat");
await polsekTerdekatState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate polsek terdekat");
}
} catch (error) {
console.error("Error updating polsek terdekat:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate polsek terdekat"
);
return false;
} finally {
polsekTerdekatState.edit.loading = false;
}
},
reset() {
polsekTerdekatState.edit.id = "";
polsekTerdekatState.edit.form = { ...defaultForm };
},
},
});
export default polsekTerdekatState;

View File

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

View File

@@ -1,339 +1,306 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
/* Introduction */
const templateIntroduction = z.object({
content: z.string().min(3, "Content minimal 3 karakter"),
})
const templateForm = z.object({
title: z.string().min(1, "Judul harus diisi"),
content: z.string().min(1, "Content harus diisi"),
introduction: z.object({
content: z.string().min(1, "Content harus diisi"),
}),
symptom: z.object({
title: z.string().min(1, "Judul harus diisi"),
content: z.string().min(1, "Content harus diisi"),
}),
prevention: z.object({
title: z.string().min(1, "Judul harus diisi"),
content: z.string().min(1, "Content harus diisi"),
}),
firstAid: z.object({
title: z.string().min(1, "Judul harus diisi"),
content: z.string().min(1, "Content harus diisi"),
}),
mythVsFact: z.object({
title: z.string().min(1, "Judul harus diisi"),
mitos: z.string().min(1, "Mitos harus diisi"),
fakta: z.string().min(1, "Fakta harus diisi"),
}),
doctorSign: z.object({
content: z.string().min(1, "Content harus diisi"),
}),
});
type Introduction = Prisma.IntroductionGetPayload<{
select: {
content: true;
};
}>;
const defaultForm = {
title: "",
content: "",
introduction: {
content: "",
},
symptom: {
title: "",
content: "",
},
prevention: {
title: "",
content: "",
},
firstAid: {
title: "",
content: "",
},
mythVsFact: {
title: "",
mitos: "",
fakta: "",
},
doctorSign: {
content: "",
},
};
const introduction = proxy({
const artikelKesehatanState = proxy({
create: {
form: {} as Introduction,
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateIntroduction.safeParse(introduction.create.form);
async submit() {
const cek = templateForm.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
const errMsg = cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n");
toast.error(errMsg);
return null;
}
try {
introduction.create.loading = true;
const res = await ApiFetch.api.kesehatan.introduction["create"].post(introduction.create.form);
this.loading = true;
const payload = { ...this.form };
const res = await (ApiFetch.api.kesehatan as any)[
"artikel-kesehatan"
].create.post(payload);
if (res.status === 200) {
introduction.findMany.load();
return toast.success("success create");
toast.success("Berhasil menambahkan artikel kesehatan");
this.resetForm();
await artikelKesehatanState.findMany.load();
return res.data;
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} catch (err: any) {
const msg = err?.message || "Terjadi kesalahan saat mengirim data";
toast.error(msg);
console.error("SUBMIT ERROR:", err);
return null;
} finally {
introduction.create.loading = false;
this.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.IntroductionGetPayload<{ omit: { isActive: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.kesehatan.introduction["find-many"].get();
if (res.status === 200) {
introduction.findMany.data = res.data?.data ?? [];
}
}
}
});
/* ======================================================================= */
/* symptom */
const templateSymptom = z.object({
title: z.string().min(3, "Title minimal 3 karakter"),
content: z.string().min(3, "Content minimal 3 karakter"),
})
type Symptom = Prisma.SymptomGetPayload<{
select: {
title: true;
content: true;
};
}>;
const symptom = proxy({
create: {
form: {} as Symptom,
loading: false,
async create() {
const cek = templateSymptom.safeParse(symptom.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
symptom.create.loading = true;
const res = await ApiFetch.api.kesehatan.symptom["create"].post(symptom.create.form);
if (res.status === 200) {
symptom.findMany.load();
return toast.success("success create");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
symptom.create.loading = false;
}
resetForm() {
this.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.SymptomGetPayload<{ omit: { isActive: true } }>[]
| Prisma.ArtikelKesehatanGetPayload<{
include: {
introduction: true;
symptom: true;
prevention: true;
firstaid: true;
mythvsfact: true;
doctorsign: true;
};
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.kesehatan.symptom["find-many"].get();
if (res.status === 200) {
symptom.findMany.data = res.data?.data ?? [];
try {
this.loading = true;
const res = await (ApiFetch.api.kesehatan as any)["artikel-kesehatan"][
"find-many"
].get();
if (res.status === 200) {
this.data = res.data?.data ?? [];
} else {
toast.error("Gagal memuat data artikel kesehatan");
}
return res;
} catch (err) {
toast.error("Terjadi error saat load data");
console.error("LOAD ERROR:", err);
throw err;
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.ArtikelKesehatanGetPayload<{
include: {
introduction: true;
symptom: true;
prevention: true;
firstaid: true;
mythvsfact: true;
doctorsign: true;
};
}> | null,
loading: false,
async load(id: string) {
const res = await fetch(`/api/kesehatan/artikel-kesehatan/${id}`);
if (res.ok) {
const data = await res.json();
artikelKesehatanState.findUnique.data = data.data ?? null;
} else {
toast.error("Gagal load data artikel kesehatan");
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
const res = await fetch(`/api/kesehatan/artikel-kesehatan/${id}`);
if (!res.ok) {
toast.error("Gagal load data artikel kesehatan");
return;
}
const result = await res.json();
const data = result.data;
artikelKesehatanState.edit.id = data.id;
artikelKesehatanState.edit.form = {
title: data.title,
content: data.content,
introduction: {
content: data.introduction.content,
},
symptom: {
title: data.symptom.title,
content: data.symptom.content,
},
prevention: {
title: data.prevention.title,
content: data.prevention.content,
},
firstAid: {
title: data.firstaid.title,
content: data.firstaid.content,
},
mythVsFact: {
title: data.mythvsfact.title,
mitos: data.mythvsfact.mitos,
fakta: data.mythvsfact.fakta,
},
doctorSign: {
content: data.doctorsign.content,
},
};
},
async submit() {
const cek = templateForm.safeParse(artikelKesehatanState.edit.form);
if (!cek.success) {
const errMsg = cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n");
toast.error(errMsg);
return null;
}
try {
artikelKesehatanState.edit.loading = true;
const payload = {
title: artikelKesehatanState.edit.form.title,
content: artikelKesehatanState.edit.form.content,
introduction: {
content: artikelKesehatanState.edit.form.introduction.content,
},
symptom: {
title: artikelKesehatanState.edit.form.symptom.title,
content: artikelKesehatanState.edit.form.symptom.content,
},
prevention: {
title: artikelKesehatanState.edit.form.prevention.title,
content: artikelKesehatanState.edit.form.prevention.content,
},
firstAid: {
title: artikelKesehatanState.edit.form.firstAid.title,
content: artikelKesehatanState.edit.form.firstAid.content,
},
mythVsFact: {
title: artikelKesehatanState.edit.form.mythVsFact.title,
mitos: artikelKesehatanState.edit.form.mythVsFact.mitos,
fakta: artikelKesehatanState.edit.form.mythVsFact.fakta,
},
doctorSign: {
content: artikelKesehatanState.edit.form.doctorSign.content,
},
};
const res = await fetch(
`/api/kesehatan/artikel-kesehatan/${artikelKesehatanState.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 artikel kesehatan");
await artikelKesehatanState.findMany.load();
return true;
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Terjadi kesalahan saat update"
);
return false;
} finally {
artikelKesehatanState.edit.loading = false;
}
},
resetForm() {
artikelKesehatanState.edit.id = "";
artikelKesehatanState.edit.form = { ...defaultForm };
},
},
delete: {
loading: false,
async byId(id: string) {
try {
artikelKesehatanState.delete.loading = true;
const res = await fetch(
`/api/kesehatan/artikel-kesehatan/del/${id}`,
{
method: "DELETE",
}
);
const result = await res.json();
if (res.ok && result.success) {
toast.success("Artikel kesehatan berhasil dihapus");
await artikelKesehatanState.findMany.load();
} else {
toast.error(result.message || "Gagal menghapus");
}
} catch {
toast.error("Terjadi kesalahan saat menghapus");
} finally {
artikelKesehatanState.delete.loading = false;
}
},
},
});
/* ======================================================================= */
/* Prevention */
const templatePrevention = z.object({
title: z.string().min(3, "Title minimal 3 karakter"),
content: z.string().min(3, "Content minimal 3 karakter"),
})
type Prevention = Prisma.PreventionGetPayload<{
select: {
title: true;
content: true;
};
}>;
const prevention = proxy({
create: {
form: {} as Prevention,
loading: false,
async create() {
const cek = templatePrevention.safeParse(prevention.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
prevention.create.loading = true;
const res = await ApiFetch.api.kesehatan.prevention["create"].post(prevention.create.form);
if (res.status === 200) {
prevention.findMany.load();
return toast.success("success create");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
prevention.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.PreventionGetPayload<{ omit: { isActive: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.kesehatan.prevention["find-many"].get();
if (res.status === 200) {
prevention.findMany.data = res.data?.data ?? [];
}
},
},
});
/* ======================================================================= */
/* First Aid */
const templateFirstAid = z.object({
title: z.string().min(3, "Title minimal 3 karakter"),
content: z.string().min(3, "Content minimal 3 karakter"),
})
type FirstAid = Prisma.FirstAidGetPayload<{
select: {
title: true;
content: true;
};
}>;
const firstAid = proxy({
create: {
form: {} as FirstAid,
loading: false,
async create() {
const cek = templateFirstAid.safeParse(firstAid.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
firstAid.create.loading = true;
const res = await ApiFetch.api.kesehatan.firstaid["create"].post(firstAid.create.form);
if (res.status === 200) {
firstAid.findMany.load();
return toast.success("success create");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
firstAid.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.FirstAidGetPayload<{ omit: { isActive: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.kesehatan.firstaid["find-many"].get();
if (res.status === 200) {
firstAid.findMany.data = res.data?.data ?? [];
}
},
},
})
/* ======================================================================= */
/* Myth vs Fact */
const templateMythFact = z.object({
title: z.string().min(3, "Title minimal 3 karakter"),
mitos: z.string().min(3, "Mitos minimal 3 karakter"),
fakta: z.string().min(3, "Fakta minimal 3 karakter"),
})
type MythFact = Prisma.MythVsFactGetPayload<{
select: {
title: true;
mitos: true;
fakta: true;
};
}>;
const mythFact = proxy({
create: {
form: {} as MythFact,
loading: false,
async create() {
const cek = templateMythFact.safeParse(mythFact.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
mythFact.create.loading = true;
const res = await ApiFetch.api.kesehatan.mythvsfact["create"].post(mythFact.create.form);
if (res.status === 200) {
mythFact.findMany.load();
return toast.success("success create");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
mythFact.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.MythVsFactGetPayload<{ omit: { isActive: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.kesehatan.mythvsfact["find-many"].get();
if (res.status === 200) {
mythFact.findMany.data = res.data?.data ?? [];
}
},
},
})
/* ======================================================================= */
/* Doctor Sign */
const templateDoctorSign = z.object({
content: z.string().min(3, "Content minimal 3 karakter"),
})
type DoctorSign = Prisma.DoctorSignGetPayload<{
select: {
content: true
}
}>
const doctorSign = proxy({
create: {
form: {} as DoctorSign,
loading: false,
async create() {
const cek = templateDoctorSign.safeParse(doctorSign.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
doctorSign.create.loading = true;
const res = await ApiFetch.api.kesehatan.doctor_sign["create"].post(doctorSign.create.form);
if (res.status === 200) {
doctorSign.findMany.load();
return toast.success("success create");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
doctorSign.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.DoctorSignGetPayload<{ omit: { isActive: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.kesehatan.doctor_sign["find-many"].get();
if (res.status === 200) {
doctorSign.findMany.data = res.data?.data ?? [];
}
},
},
})
/* ======================================================================= */
const stateArtikelKesehatan = proxy({
introduction,
symptom,
prevention,
firstAid,
mythFact,
doctorSign
})
export default stateArtikelKesehatan
export default artikelKesehatanState;

View File

@@ -39,15 +39,15 @@ const grafikkepuasan = proxy({
const res = await ApiFetch.api.kesehatan.grafikkepuasan["create"].post(grafikkepuasan.create.form);
if (res.status === 200) {
const uuid = res.data?.data?.uuid;
if (uuid) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikkepuasan.create.form = {
label: "",
jumlah: "",
};
grafikkepuasan.findMany.load();
return uuid;
return id;
}
}
toast.error("failed create");
@@ -77,9 +77,9 @@ const grafikkepuasan = proxy({
data: null as Prisma.GrafikKepuasanGetPayload<{
omit: { isActive: true }
}> | null,
async load(uuid: string) {
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/grafikkepuasan/${uuid}`);
const res = await fetch(`/api/kesehatan/grafikkepuasan/${id}`);
if (res.ok) {
const data = await res.json();
grafikkepuasan.findUnique.data = data.data ?? null;
@@ -94,14 +94,14 @@ const grafikkepuasan = proxy({
},
},
update: {
uuid: "",
id: "",
form: {...defaultForm},
loading: false,
async byId() {
},
async submit() {
const uuid = this.uuid;
if (!uuid) {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
@@ -114,7 +114,7 @@ const grafikkepuasan = proxy({
}
try {
this.loading = true;
const response = await fetch(`/api/kesehatan/grafikkepuasan/${uuid}`, {
const response = await fetch(`/api/kesehatan/grafikkepuasan/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
@@ -143,14 +143,14 @@ const grafikkepuasan = proxy({
},
delete: {
loading: false,
async byId(uuid: string) {
if (!uuid) {
async byId(id: string) {
if (!id) {
return toast.warn("ID tidak valid");
}
try {
grafikkepuasan.delete.loading = true;
const response = await fetch(`/api/kesehatan/grafikkepuasan/del/${uuid}`, {
const response = await fetch(`/api/kesehatan/grafikkepuasan/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",

View File

@@ -45,12 +45,12 @@ const persentasekelahiran = proxy({
);
if (res.status === 200) {
const uuid = res.data?.data?.uuid;
if (uuid) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
persentasekelahiran.create.form = { ...defaultForm };
persentasekelahiran.findMany.load();
return uuid;
return id;
}
}
toast.error("failed create");
@@ -80,9 +80,9 @@ const persentasekelahiran = proxy({
data: null as Prisma.DataKematian_KelahiranGetPayload<{
omit: { isActive: true };
}> | null,
async load(uuid: string) {
async load(id: string) {
try {
const res = await fetch(`/api/kesehatan/persentasekelahiran/${uuid}`);
const res = await fetch(`/api/kesehatan/persentasekelahiran/${id}`);
if (res.ok) {
const data = await res.json();
persentasekelahiran.findUnique.data = data.data ?? null;
@@ -98,13 +98,13 @@ const persentasekelahiran = proxy({
},
update: {
uuid: "",
id: "",
form: { ...defaultForm },
loading: false,
async submit() {
const uuid = this.uuid;
if (!uuid) {
toast.warn("UUID tidak valid");
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
@@ -126,7 +126,7 @@ update: {
try {
this.loading = true;
const res = await fetch(`/api/kesehatan/persentasekelahiran/${uuid}`, {
const res = await fetch(`/api/kesehatan/persentasekelahiran/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
@@ -156,13 +156,13 @@ update: {
delete: {
loading: false,
async byId(uuid: string) {
if (!uuid) return toast.warn("UUID tidak valid");
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
persentasekelahiran.delete.loading = true;
const response = await fetch(`/api/kesehatan/persentasekelahiran/del/${uuid}`, {
const response = await fetch(`/api/kesehatan/persentasekelahiran/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
@@ -177,7 +177,7 @@ update: {
} else {
toast.error(result?.message || "Gagal menghapus persentase kelahiran");
}
} catch (error) {
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus persentase kelahiran");
} finally {

View File

@@ -35,9 +35,7 @@ function DetailBerita() {
if (!beritaState.berita.findUnique.data) {
return (
<Stack py={10}>
{Array.from({ length: 10 }).map((_, k) => (
<Skeleton key={k} h={40} />
))}
<Skeleton h={40} />
</Stack>
)
}

View File

@@ -0,0 +1,155 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
import colors from '@/con/colors';
import { Box, Button, 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 EditLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState)
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
posisi: lowonganKerjaState.update.form.posisi,
namaPerusahaan: lowonganKerjaState.update.form.namaPerusahaan,
lokasi: lowonganKerjaState.update.form.lokasi,
tipePekerjaan: lowonganKerjaState.update.form.tipePekerjaan,
gaji: lowonganKerjaState.update.form.gaji,
deskripsi: lowonganKerjaState.update.form.deskripsi,
kualifikasi: lowonganKerjaState.update.form.kualifikasi,
})
useEffect(() => {
const loadLowongan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await lowonganState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
posisi: data.posisi || '',
namaPerusahaan: data.namaPerusahaan || '',
lokasi: data.lokasi || '',
tipePekerjaan: data.tipePekerjaan || '',
gaji: data.gaji || '',
deskripsi: data.deskripsi || '',
kualifikasi: data.kualifikasi || '',
});
}
} catch (error) {
console.error("Error loading lowongan kerja:", error);
toast.error("Gagal memuat data lowongan kerja");
}
};
loadLowongan();
}, [params?.id])
const handleSubmit = async () => {
try {
lowonganKerjaState.update.form = {
...lowonganKerjaState.update.form,
posisi: formData.posisi,
namaPerusahaan: formData.namaPerusahaan,
lokasi: formData.lokasi,
tipePekerjaan: formData.tipePekerjaan,
gaji: formData.gaji,
deskripsi: formData.deskripsi,
kualifikasi: formData.kualifikasi,
}
await lowonganState.update.update()
toast.success("Lowongan kerja berhasil diperbarui!");
router.push("/admin/ekonomi/lowongan-kerja-lokal");
} catch (error) {
console.error("Error updating lowongan kerja:", error);
toast.error("Terjadi kesalahan saat memperbarui lowongan kerja");
}
}
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 Lowongan Kerja Lokal</Title>
<TextInput
value={formData.posisi}
onChange={(val) => {
formData.posisi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>}
placeholder='Masukkan posisi'
/>
<TextInput
value={formData.namaPerusahaan}
onChange={(val) => {
formData.namaPerusahaan = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>}
placeholder='Masukkan nama perusahaan'
/>
<TextInput
value={formData.lokasi}
onChange={(val) => {
formData.lokasi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
placeholder='Masukkan lokasi'
/>
<TextInput
value={formData.tipePekerjaan}
onChange={(val) => {
formData.tipePekerjaan = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>}
placeholder='Masukkan tipe pekerjaan'
/>
<TextInput
value={formData.gaji}
onChange={(val) => {
formData.gaji = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
placeholder='Masukkan gaji'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
formData.deskripsi = val;
}}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Kualifikasi Lowongan Kerja</Text>
<EditEditor
value={formData.kualifikasi}
onChange={(val) => {
formData.kualifikasi = val;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditLowonganKerja;

View File

@@ -0,0 +1,128 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
function DetailLowonganKerjaLokal() {
const lowonganState = useProxy(lowonganKerjaState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
lowonganState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
lowonganState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/lowongan-kerja-lokal")
}
}
if (!lowonganState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
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']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Lowongan Kerja Lokal</Text>
{lowonganState.findUnique.data ? (
<Paper key={lowonganState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Bekerja Sebagai</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.posisi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Usaha</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.namaPerusahaan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Lokasi</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.lokasi}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tipe Pekerjaan</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.tipePekerjaan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gaji</Text>
<Text fz={"lg"}>{lowonganState.findUnique.data?.gaji}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: lowonganState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kualifikasi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: lowonganState.findUnique.data?.kualifikasi }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (lowonganState.findUnique.data) {
setSelectedId(lowonganState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={lowonganState.delete.loading || !lowonganState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (lowonganState.findUnique.data) {
router.push(`/admin/ekonomi/lowongan-kerja-lokal/${lowonganState.findUnique.data.id}/edit`);
}
}}
disabled={!lowonganState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus lowongan kerja ini?'
/>
</Box>
);
}
export default DetailLowonganKerjaLokal;

View File

@@ -3,11 +3,33 @@ import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
function CreateLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState)
const router = useRouter();
const resetForm = () => {
lowonganState.create.form = {
posisi: "",
namaPerusahaan: "",
lokasi: "",
tipePekerjaan: "",
gaji: "",
deskripsi: "",
kualifikasi: "",
}
}
const handleSubmit = async () => {
await lowonganState.create.create()
resetForm()
router.push("/admin/ekonomi/lowongan-kerja-lokal")
}
return (
<Box>
<Box mb={10}>
@@ -20,33 +42,65 @@ function CreateLowonganKerja() {
<Stack gap={"xs"}>
<Title order={4}>Create Lowongan Kerja Lokal</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pekerjaan</Text>}
placeholder='Masukkan pekerjaan'
value={lowonganState.create.form.posisi}
onChange={(val) => {
lowonganState.create.form.posisi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Posisi</Text>}
placeholder='Masukkan posisi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Usaha</Text>}
placeholder='Masukkan nama usaha'
value={lowonganState.create.form.namaPerusahaan}
onChange={(val) => {
lowonganState.create.form.namaPerusahaan = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Perusahaan</Text>}
placeholder='Masukkan nama perusahaan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
value={lowonganState.create.form.lokasi}
onChange={(val) => {
lowonganState.create.form.lokasi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Lokasi</Text>}
placeholder='Masukkan lokasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nomor Telepon</Text>}
placeholder='Masukkan nomor telepon'
value={lowonganState.create.form.tipePekerjaan}
onChange={(val) => {
lowonganState.create.form.tipePekerjaan = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Tipe Pekerjaan</Text>}
placeholder='Masukkan tipe pekerjaan'
/>
<TextInput
value={lowonganState.create.form.gaji}
onChange={(val) => {
lowonganState.create.form.gaji = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
placeholder='Masukkan gaji'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text>
<KeamananEditor
showSubmit={false}
<CreateEditor
value={lowonganState.create.form.deskripsi}
onChange={(val) => {
lowonganState.create.form.deskripsi = val;
}}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Kualifikasi Lowongan Kerja</Text>
<CreateEditor
value={lowonganState.create.form.kualifikasi}
onChange={(val) => {
lowonganState.create.form.kualifikasi = val;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,78 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailLowonganKerjaLokal() {
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 Lowongan Kerja Lokal</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Bekerja Sebagai</Text>
<Text>Karyawan</Text>
</Box>
<Box>
<Text fw={"bold"}>Nama Usaha</Text>
<Text>BIBD</Text>
</Box>
<Box>
<Text fw={"bold"}>Alamat Usaha</Text>
<Text>Jalan In Aja</Text>
</Box>
<Box>
<Text fw={"bold"}>Nomor Telepon</Text>
<Text>0896232831883</Text>
</Box>
<Box>
<Text fw={"bold"}>Waktu Kerja</Text>
<Text>Full Time</Text>
</Box>
<Box>
<Text fw={"bold"}>Gaji selama 1 bulan</Text>
<Text>Rp. 3.000.000</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Lowongan Kerja</Text>
<Text> Pekerjaan dengan gaji Rp. 3.000.000</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/lowongan-kerja-lokal/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 DetailLowonganKerjaLokal;

View File

@@ -1,57 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function EditLowonganKerja() {
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 Lowongan Kerja Lokal</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pekerjaan</Text>}
placeholder='Masukkan pekerjaan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Usaha</Text>}
placeholder='Masukkan nama usaha'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nomor Telepon</Text>}
placeholder='Masukkan nomor telepon'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Gaji selama 1 bulan</Text>}
placeholder='Masukkan gaji'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Lowongan Kerja</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditLowonganKerja;

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, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } 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 lowonganKerjaState from '../../_state/ekonomi/lowongan-kerja';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
function LowonganKerjaLokal() {
return (
@@ -20,7 +23,20 @@ function LowonganKerjaLokal() {
}
function ListLowonganKerjaLokal() {
const lowonganState = useProxy(lowonganKerjaState)
const router = useRouter();
useShallowEffect(() => {
lowonganState.findMany.load();
}, [])
if (!lowonganState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -38,16 +54,18 @@ function ListLowonganKerjaLokal() {
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Karyawan</TableTd>
<TableTd>BIBD</TableTd>
<TableTd>Jalan In Aja</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/lowongan-kerja-lokal/detail')}>
{lowonganState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.posisi}</TableTd>
<TableTd>{item.namaPerusahaan}</TableTd>
<TableTd>{item.lokasi}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>

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 LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Produk Pasar Desa",
value: "produkpasardesa",
href: "/admin/ekonomi/pasar-desa/produk-pasar-desa"
},
{
label: "Kategori Produk",
value: "kategoriproduk",
href: "/admin/ekonomi/pasar-desa/kategori-produk"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const 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}>Pasar 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 LayoutTabs;

View File

@@ -1,56 +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 { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function CreatePasarDesa() {
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}>Create Pasar Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Produk</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePasarDesa;

View File

@@ -1,74 +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 DetailPasarDesa() {
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 Pasar Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Produk</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Harga Produk</Text>
<Text fz={"lg"}>Rp. 20.000</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Rating Produk</Text>
<Text fz={"lg"}>5</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat Usaha</Text>
<Text fz={"lg"}>Jalan In Aja</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} >Test Konten</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/pasar-desa/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 DetailPasarDesa;

View File

@@ -1,56 +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 { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function EditPasarDesa() {
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 Pasar Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Produk</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPasarDesa;

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React, { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { toast } from 'react-toastify';
function EditKategoriProduk() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [formData, setFormData] = useState({
nama: "",
});
useEffect(() => {
const loadKategoriProduk = async () => {
if (!id) return;
try {
const data = await statePasar.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
statePasar.edit.id = id;
setFormData({
nama: data.nama || '',
});
}
} catch (error) {
console.error("Error loading kategori produk:", error);
toast.error("Gagal memuat data kategori produk");
}
};
loadKategoriProduk();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
toast.error('Nama kategori produk tidak boleh kosong');
return;
}
statePasar.edit.form = {
nama: formData.nama.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!statePasar.edit.id) {
statePasar.edit.id = id; // fallback
}
const success = await statePasar.edit.update();
if (success) {
router.push("/admin/ekonomi/pasar-desa/kategori-produk");
}
} catch (error) {
console.error("Error updating kategori produk:", error);
// toast akan ditampilkan dari fungsi update
}
};
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 Kategori Produk</Title>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Produk</Text>}
placeholder='Masukkan nama kategori produk'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKategoriProduk;

View File

@@ -0,0 +1,61 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React, { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function CreateKategoriProduk() {
const router = useRouter();
const statePasar = useProxy(pasarDesaState.kategoriProduk)
useEffect(() => {
statePasar.findMany.load();
}, []);
const resetForm = () => {
statePasar.create.form = {
nama: "",
};
}
const handleSubmit = async () => {
await statePasar.create.create();
resetForm();
router.push("/admin/ekonomi/pasar-desa/kategori-produk")
}
return (
<Box>
<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}>Create Kategori Produk</Title>
<TextInput
value={statePasar.create.form.nama}
onChange={(val) => {
statePasar.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Produk</Text>}
placeholder='Masukkan nama kategori produk'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateKategoriProduk;

View File

@@ -0,0 +1,102 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
function PasarDesa() {
return (
<Box>
<HeaderSearch
title='Kategori Produk'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListPasarDesa />
</Box>
);
}
function ListPasarDesa() {
const statePasar = useProxy(pasarDesaState.kategoriProduk)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
// const params = useParams()
const router = useRouter()
useShallowEffect(() => {
statePasar.findMany.load()
}, [])
const handleHapus = () => {
if (selectedId) {
statePasar.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
if (!statePasar.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Produk Pasar Desa'
href='/admin/ekonomi/pasar-desa/kategori-produk/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{statePasar.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/pasar-desa/kategori-produk/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus kategori produk ini?'
/>
</Box>
);
}
export default PasarDesa;

View File

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

View File

@@ -1,60 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } 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';
function PasarDesa() {
return (
<Box>
<HeaderSearch
title='Pasar Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListPasarDesa/>
</Box>
);
}
function ListPasarDesa() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pasar Desa'
href='/admin/ekonomi/pasar-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Produk</TableTh>
<TableTh>Harga Produk</TableTh>
<TableTh>Rating Produk</TableTh>
<TableTh>Alamat Usaha</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Produk 1</TableTd>
<TableTd>Harga Rp. 20.000</TableTd>
<TableTd>Rating 5</TableTd>
<TableTd>Jalan In Aja</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/pasar-desa/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default PasarDesa;

View File

@@ -0,0 +1,226 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPasarDesa() {
const pasarState = useProxy(pasarDesaState)
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
nama: pasarState.pasarDesa.edit.form.nama || "",
harga: pasarState.pasarDesa.edit.form.harga || 0,
alamatUsaha: pasarState.pasarDesa.edit.form.alamatUsaha || "",
imageId: pasarState.pasarDesa.edit.form.imageId || "",
rating: pasarState.pasarDesa.edit.form.rating || 0,
kategoriId: pasarState.pasarDesa.edit.form.kategoriId || [],
})
useEffect(() => {
pasarState.kategoriProduk.findMany.load();
const loadPasarDesa = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await pasarState.pasarDesa.edit.load(id);
if (data) {
setFormData({
nama: data.nama || "",
harga: data.harga || 0,
alamatUsaha: data.alamatUsaha || "",
imageId: data.imageId || "",
rating: data.rating || 0,
// Use the IDs from KategoriToPasar relationship
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
});
// Tampilkan preview gambar
if (data.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading pasar desa:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengambil data pasar desa"
);
}
}
loadPasarDesa();
}, [params?.id]);
const handleSubmit = async () => {
try {
pasarState.pasarDesa.edit.form = {
...pasarState.pasarDesa.edit.form,
nama: formData.nama,
harga: formData.harga,
alamatUsaha: formData.alamatUsaha,
imageId: formData.imageId,
rating: formData.rating,
kategoriId: formData.kategoriId,
}
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");
}
// Update imageId in global state
pasarState.pasarDesa.edit.form.imageId = uploaded.id;
}
await pasarState.pasarDesa.edit.update();
toast.success("Tips Keamanan berhasil diperbarui!");
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa");
} catch (error) {
console.error("Error updating pasar desa:", error);
toast.error("Terjadi kesalahan saat memperbarui pasar desa");
}
};
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 Pasar Desa</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
value={formData.harga}
onChange={(e) => setFormData({ ...formData, harga: Number(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
type="number"
min={0}
max={5}
step={0.1} // bisa pakai 0.1 biar support desimal
value={formData.rating}
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk (0-5)'
/>
<TextInput
value={formData.alamatUsaha}
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<TextInput
value={formData.alamatUsaha}
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<MultiSelect
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val })}
label={<Text fw={"bold"} fz={"sm"}>Kategori Produk</Text>}
placeholder='Pilih kategori produk'
data={
pasarState.kategoriProduk.findMany.data?.map((v) => ({
value: v.id, // Make sure this is using the ID
label: v.nama
})) || []
}
clearable
searchable
required
error={!formData.kategoriId.length ? "Pilih minimal satu kategori" : undefined}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPasarDesa;

View File

@@ -0,0 +1,131 @@
'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 { useRouter, useParams } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
function DetailPasarDesa() {
const statePasar = useProxy(pasarDesaState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter();
useShallowEffect(() => {
statePasar.pasarDesa.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
statePasar.pasarDesa.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa")
}
}
if (!statePasar.pasarDesa.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 Pasar Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Produk</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Harga Produk</Text>
<Text fz={"lg"}>Rp.{statePasar.pasarDesa.findUnique.data?.harga}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Rating Produk</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.rating}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat Usaha</Text>
<Text fz={"lg"}>{statePasar.pasarDesa.findUnique.data?.alamatUsaha}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={statePasar.pasarDesa.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kategori</Text>
<Stack gap={4}>
{statePasar.pasarDesa.findUnique.data?.KategoriToPasar &&
statePasar.pasarDesa.findUnique.data.KategoriToPasar.length > 0 ? (
statePasar.pasarDesa.findUnique.data.KategoriToPasar.map((kategori) => (
<Text fz={"lg"} key={kategori.id}>
{kategori.kategori.nama}
</Text>
))
) : (
<Text fz={"lg"} c="dimmed">
Tidak ada kategori
</Text>
)}
</Stack>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (statePasar.pasarDesa.findUnique.data) {
setSelectedId(statePasar.pasarDesa.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={!statePasar.pasarDesa.findUnique.data}
color="red">
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (statePasar.pasarDesa.findUnique.data) {
router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${statePasar.pasarDesa.findUnique.data.id}/edit`);
}
}}
disabled={!statePasar.pasarDesa.findUnique.data}
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 produk ini?"
/>
</Box>
);
}
export default DetailPasarDesa;

View File

@@ -0,0 +1,192 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, MultiSelect, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
function CreatePasarDesa() {
const router = useRouter();
const statePasar = useProxy(pasarDesaState)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
statePasar.kategoriProduk.findMany.load();
}, []);
const resetForm = () => {
statePasar.pasarDesa.create.form = {
nama: "",
harga: 0,
alamatUsaha: "",
imageId: "",
rating: 0,
kategoriId: [],
};
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 mengupload file");
}
statePasar.pasarDesa.create.form.imageId = uploaded.id;
await statePasar.pasarDesa.create.create();
resetForm();
router.push("/admin/ekonomi/pasar-desa/produk-pasar-desa")
}
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}>Create Pasar Desa</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
value={statePasar.pasarDesa.create.form.nama}
onChange={(val) => {
statePasar.pasarDesa.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Produk</Text>}
placeholder='Masukkan nama produk'
/>
<TextInput
value={statePasar.pasarDesa.create.form.harga}
onChange={(val) => {
statePasar.pasarDesa.create.form.harga = Number(val.target.value);
}}
label={<Text fw={"bold"} fz={"sm"}>Harga Produk</Text>}
placeholder='Masukkan harga produk'
/>
<TextInput
type="number"
min={0}
max={5}
step={0.1} // bisa pakai 0.1 biar support desimal
value={statePasar.pasarDesa.create.form.rating}
onChange={(val) => {
const value = Number(val.target.value);
// Validasi manual juga boleh (jaga-jaga)
if (value >= 0 && value <= 5) {
statePasar.pasarDesa.create.form.rating = value;
}
}}
label={<Text fw={"bold"} fz={"sm"}>Rating Produk</Text>}
placeholder='Masukkan rating produk (0-5)'
/>
<TextInput
value={statePasar.pasarDesa.create.form.alamatUsaha}
onChange={(val) => {
statePasar.pasarDesa.create.form.alamatUsaha = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Alamat Usaha</Text>}
placeholder='Masukkan alamat usaha'
/>
<MultiSelect
value={statePasar.pasarDesa.create.form.kategoriId}
onChange={(val) => {
statePasar.pasarDesa.create.form.kategoriId = val;
}}
label={<Text fw={"bold"} fz={"sm"}>Kategori Produk</Text>}
placeholder='Pilih kategori produk'
data={
statePasar.kategoriProduk.findMany.data?.map((v) => ({
value: v.id,
label: v.nama
})) || []
}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePasarDesa;

View File

@@ -0,0 +1,79 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa';
function PasarDesa() {
return (
<Box>
<HeaderSearch
title='Produk Pasar Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListPasarDesa />
</Box>
);
}
function ListPasarDesa() {
const statePasar = useProxy(pasarDesaState.pasarDesa)
const router = useRouter();
useShallowEffect(() => {
statePasar.findMany.load()
}, [])
if (!statePasar.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Produk Pasar Desa'
href='/admin/ekonomi/pasar-desa/produk-pasar-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Produk</TableTh>
<TableTh>Harga Produk</TableTh>
<TableTh>Rating Produk</TableTh>
<TableTh>Alamat Usaha</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{statePasar.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>Rp.{item.harga}</TableTd>
<TableTd>{item.rating}</TableTd>
<TableTd>{item.alamatUsaha}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/pasar-desa/produk-pasar-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default PasarDesa;

View File

@@ -0,0 +1,126 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function EditProgramKemiskinan() {
const router = useRouter();
const params = useParams() as { id: string };
const stateProgram = useProxy(programKemiskinanState);
const id = params.id;
useEffect(() => {
if (id) {
stateProgram.findUnique.load(id).then(() => {
const data = stateProgram.findUnique.data;
if (data) {
stateProgram.update.form = {
nama: data.nama || '',
deskripsi: data.deskripsi || '',
ikonUrl: data.ikonUrl || '',
statistik: {
tahun: data.statistik?.tahun?.toString() || '',
jumlah: data.statistik?.jumlah?.toString() || '',
},
};
}
});
}
}, [id]);
const handleSubmit = async () => {
stateProgram.update.id = id;
await stateProgram.update.update();
router.push('/admin/ekonomi/program-kemiskinan');
};
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 Program Kemiskinan</Title>
<TextInput
value={stateProgram.update.form.nama}
onChange={(e) => {
stateProgram.update.form.nama = e.target.value;
}}
label={<Text fw="bold" fz="md">Judul Program</Text>}
placeholder="Masukkan judul program"
/>
<Box>
<Text fw="bold" fz="md">Deskripsi</Text>
<EditEditor
value={stateProgram.update.form.deskripsi}
onChange={(val) => {
stateProgram.update.form.deskripsi = val;
}}
/>
</Box>
<TextInput
value={stateProgram.update.form.ikonUrl}
onChange={(e) => {
stateProgram.update.form.ikonUrl = e.target.value;
}}
label={<Text fw="bold" fz="md">Ikon URL</Text>}
placeholder="Masukkan ikon url"
/>
<Text fw="bold" fz="md">Statistik Jumlah Masyarakat Miskin</Text>
<TextInput
type="number"
value={stateProgram.update.form.statistik.jumlah}
onChange={(e) => {
stateProgram.update.form.statistik.jumlah = e.target.value;
}}
label={<Text fw="bold" fz="md">Jumlah Masyarakat Miskin</Text>}
placeholder="Masukkan jumlah masyarakat miskin"
/>
<TextInput
type="number"
value={stateProgram.update.form.statistik.tahun}
onChange={(e) => {
stateProgram.update.form.statistik.tahun = e.target.value;
}}
label={<Text fw="bold" fz="md">Tahun</Text>}
placeholder="Masukkan tahun"
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKemiskinan;

View File

@@ -0,0 +1,115 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
function DetailProgramKemiskinan() {
const programState = useProxy(programKemiskinanState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const params = useParams()
useShallowEffect(() => {
programState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
programState.delete.delete(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/program-kemiskinan")
}
}
if (!programState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</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 Kemiskinan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Judul Program</Text>
<Text fz={"md"}>{programState.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: programState.findUnique.data?.deskripsi }}></Text>
</Box>
<Box>
<Text fw={"bold"}>Ikon URL</Text>
<Text fz={"md"}>{programState.findUnique.data?.ikonUrl}</Text>
</Box>
<Text fw={"bold"}>Statistik Jumlah Masyarakat Miskin</Text>
<Box>
<Text fw={"bold"}>Jumlah Masyarakat Miskin</Text>
<Text fz={"md"}>{programState.findUnique.data?.statistik?.jumlah}</Text>
</Box>
<Box>
<Text fw={"bold"}>Tahun</Text>
<Text fz={"md"}>{programState.findUnique.data?.statistik?.tahun}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (programState.findUnique.data) {
setSelectedId(programState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={programState.delete.loading || !programState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (programState.findUnique.data) {
router.push(`/admin/ekonomi/program-kemiskinan/${programState.findUnique.data.id}/edit`);
}
}}
disabled={!programState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kemiskinan ini?"
/>
</Box>
);
}
export default DetailProgramKemiskinan;

View File

@@ -1,45 +1,107 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
import CreateEditor from '../../../_com/createEditor';
import { useState } from 'react';
function CreateProgramKemiskinan() {
const programState = useProxy(programKemiskinanState)
const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]);
const resetForm = () => {
programState.create.form = {
nama: "",
deskripsi: "",
ikonUrl: "",
statistik: {
tahun: "",
jumlah: "",
}
}
}
const handleSubmit = async () => {
const id = await programState.create.create();
if (id) {
const idStr = String(id);
await programState.findUnique.load(idStr);
if (programState.findUnique.data) {
setLineChart([programState.findUnique.data]);
}
}
resetForm()
router.push("/admin/ekonomi/program-kemiskinan")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Program Kemiskinan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Program</Text>}
placeholder='Masukkan judul program'
value={programState.create.form.nama}
onChange={(val) => {
programState.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Judul Program</Text>}
placeholder='Masukkan judul program'
/>
<Box>
<Text fw={"bold"} fz={"md"}>Deskripsi</Text>
<CreateEditor
value={programState.create.form.deskripsi}
onChange={(val) => {
programState.create.form.deskripsi = val;
}}
/>
</Box>
<TextInput
value={programState.create.form.ikonUrl}
onChange={(val) => {
programState.create.form.ikonUrl = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Ikon URL</Text>}
placeholder='Masukkan ikon url'
/>
<Text fw={"bold"} fz={"md"}>Statistik Jumlah Masyarakat Miskin</Text>
<TextInput
type='number'
value={programState.create.form.statistik.jumlah}
onChange={(val) => {
programState.create.form.statistik.jumlah = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Jumlah Masyarakat Miskin</Text>}
placeholder='Masukkan jumlah masyarakat miskin'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
placeholder='Masukkan deskripsi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Masyarakat Miskin</Text>}
placeholder='Masukkan jumlah masyarakat miskin'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
type='number'
value={programState.create.form.statistik.tahun}
onChange={(val) => {
programState.create.form.statistik.tahun = val.target.value;
}}
label={<Text fw={"bold"} fz={"md"}>Tahun</Text>}
placeholder='Masukkan tahun'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

@@ -1,66 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKemiskinan() {
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 Program Kemiskinan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Judul Program</Text>
<Text>Program A</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Singkat</Text>
<Text>Deskripsi Program A</Text>
</Box>
<Box>
<Text fw={"bold"}>Jumlah Masyarakat Miskin</Text>
<Text>100</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi</Text>
<Text>Deskripsi Program A</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/program-kemiskinan/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 DetailProgramKemiskinan;

View File

@@ -1,46 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditProgramKemiskinan() {
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 Program Kemiskinan</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Program</Text>}
placeholder='Masukkan judul program'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat</Text>}
placeholder='Masukkan deskripsi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah Masyarakat Miskin</Text>}
placeholder='Masukkan jumlah masyarakat miskin'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKemiskinan;

View File

@@ -1,10 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'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, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } 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 programKemiskinanState from '../../_state/ekonomi/program-kemiskinan';
import { useShallowEffect } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { CartesianGrid, Legend, Line, LineChart, Tooltip, XAxis, YAxis } from 'recharts';
function ProgramKemiskinan() {
return (
@@ -14,13 +20,44 @@ function ProgramKemiskinan() {
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListProgramKemiskinan/>
<ListProgramKemiskinan />
</Box>
);
}
function ListProgramKemiskinan() {
const programState = useProxy(programKemiskinanState)
const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
useShallowEffect(() => {
setMounted(true)
programState.findMany.load()
}, [])
useEffect(() => {
if (programState.findMany.data) {
const chartData = programState.findMany.data
.filter(item => item.statistik)
.map(item => ({
tahun: item.statistik?.tahun,
jumlah: Number(item.statistik?.jumlah)
}))
.sort((a, b) => (a.tahun || 0) - (b.tahun || 0)); // opsional, urutkan tahun
setLineChart(chartData);
}
}, [programState.findMany.data])
if (!programState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -35,22 +72,66 @@ function ListProgramKemiskinan() {
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Jumlah Masyarakat Miskin</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Program A</TableTd>
<TableTd>Deskripsi Program A</TableTd>
<TableTd>100</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/program-kemiskinan/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
{programState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Text fz={'sm'} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>{item.statistik?.jumlah}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Table>
</Paper>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'} >
<Stack>
<Box >
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
{mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}>
<Box w={"100%"} style={{overflowX: 'auto'}}>
<LineChart
width={820}
height={300}
data={lineChart}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="tahun" />
<YAxis />
<Tooltip
formatter={(value: any, name: string) => [`${value} orang`, name]}
labelFormatter={(label: any) => `Tahun: ${label}`}
/>
<Legend />
<Line
type="monotone"
dataKey="jumlah"
name="Jumlah per Tahun"
stroke={colors['blue-button']}
/>
</LineChart>
</Box>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Box>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,179 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import {
Box,
Button,
Center,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title
} from "@mantine/core";
import { IconArrowBack, IconImageInPicture, 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";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { Dropzone } from "@mantine/dropzone";
import keamananLingkunganState from "../../../../_state/keamanan/keamanan-lingkungan";
function EditKeamananLingkungan() {
const keamananState = useProxy(keamananLingkunganState);
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: keamananState.edit.form.name || '',
deskripsi: keamananState.edit.form.deskripsi || '',
imageId: keamananState.edit.form.imageId || ''
});
// Load berita by id saat pertama kali
useEffect(() => {
const loadBerita = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await keamananState.edit.load(id); // akses langsung, bukan dari proxy
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 keamananLingkungan:", error);
toast.error("Gagal memuat data keamananLingkungan");
}
};
loadBerita();
}, [params?.id]); // ✅ hapus beritaState dari dependency
const handleSubmit = async () => {
try {
// Update global state with form data
keamananState.edit.form = {
...keamananState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId // Keep existing imageId if not changed
};
// Jika ada file baru, upload
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");
}
// Update imageId in global state
keamananState.edit.form.imageId = uploaded.id;
}
await keamananState.edit.update();
toast.success("Keamanan Lingkungan berhasil diperbarui!");
router.push("/admin/keamanan/keamanan-lingkungan-pecalang-patwal");
} catch (error) {
console.error("Error updating keamananLingkungan:", error);
toast.error("Terjadi kesalahan saat memperbarui keamananLingkungan");
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Keamanan Lingkungan</Title>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul Keamanan Lingkungan</Text>}
placeholder="masukkan judul"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
keamananState.edit.form.deskripsi = htmlContent;
}}
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditKeamananLingkungan;

View File

@@ -0,0 +1,111 @@
'use client'
import { useProxy } from 'valtio/utils';
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 colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import keamananLingkunganState from '../../../_state/keamanan/keamanan-lingkungan';
function DetailKeamananLingkungan() {
const keamananState = useProxy(keamananLingkunganState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
keamananState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
keamananState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/keamanan/keamanan-lingkungan-pecalang-patwal")
}
}
if (!keamananState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
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']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Keamanan Lingkungan</Text>
{keamananState.findUnique.data ? (
<Paper key={keamananState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Keamanan Lingkungan</Text>
<Text fz={"lg"}>{keamananState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: keamananState.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={keamananState.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (keamananState.findUnique.data) {
setSelectedId(keamananState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={keamananState.delete.loading || !keamananState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (keamananState.findUnique.data) {
router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${keamananState.findUnique.data.id}/edit`);
}
}}
disabled={!keamananState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus keamanan lingkungan ini?'
/>
</Box>
);
}
export default DetailKeamananLingkungan;

View File

@@ -1,43 +1,147 @@
'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 ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../_com/keamananEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import keamananLingkunganState from '../../../_state/keamanan/keamanan-lingkungan';
function CreateKeamananLingkungan() {
const keamananState = useProxy(keamananLingkunganState)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter();
const resetForm = () => {
keamananState.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 mengupload file");
}
keamananState.create.form.imageId = uploaded.id;
await keamananState.create.create();
resetForm();
router.push("/admin/keamanan/keamanan-lingkungan-pecalang-patwal")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Keamanan Lingkungan</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Keamanan Lingkungan</Text>}
placeholder='Masukkan nama KeamananLingkungan'
value={keamananState.create.form.name}
onChange={(val) => {
keamananState.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Keamanan Lingkungan</Text>}
placeholder='Masukkan nama Keamanan Lingkungan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi KeamananLingkungan</Text>
<KeamananEditor
showSubmit={false}
<Text fw={"bold"} fz={"sm"}>Deskripsi Keamanan Lingkungan</Text>
<CreateEditor
value={keamananState.create.form.deskripsi}
onChange={(val) => {
keamananState.create.form.deskripsi = val;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</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 DetailKeamananLingkungan() {
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 Keamanan Lingkungan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Keamanan Lingkungan</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nomor Keamanan Lingkungan</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/keamanan/keamanan-lingkungan-pecalang-patwal/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 DetailKeamananLingkungan;

View File

@@ -1,44 +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 { KeamananEditor } from '../../_com/keamananEditor';
function EditKeamananLingkungan() {
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 Keamanan Lingkungan</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Keamanan Lingkungan</Text>}
placeholder='Masukkan nama Keamanan Lingkungan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Keamanan Lingkungan</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKeamananLingkungan;

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, Stack, 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 keamananLingkunganState from '../../_state/keamanan/keamanan-lingkungan';
import { useShallowEffect } from '@mantine/hooks';
function KeamananLingkungan() {
return (
@@ -14,13 +17,26 @@ function KeamananLingkungan() {
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListKeamananLingkungan/>
<ListKeamananLingkungan />
</Box>
);
}
function ListKeamananLingkungan() {
const keamananState = useProxy(keamananLingkunganState)
const router = useRouter();
useShallowEffect(() => {
keamananState.findMany.load()
}, [])
if (!keamananState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -32,24 +48,26 @@ function ListKeamananLingkungan() {
<TableThead>
<TableTr>
<TableTh>Nama Keamanan Lingkungan</TableTh>
<TableTh>Nomor Keamanan Lingkungan</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Keamanan Lingkungan 1</TableTd>
<TableTd>0896232831883</TableTd>
<TableTd>Keamanan Lingkungan 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
{keamananState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Table>
</Paper>
</Box>
);

View File

@@ -0,0 +1,269 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import kontakDaruratKeamananState from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKontakDarurat() {
const router = useRouter();
const kontakState = useProxy(kontakDaruratKeamananState)
const params = useParams()
const [previewUtama, setPreviewUtama] = useState<string | null>(null);
const [fileUtama, setFileUtama] = useState<File | null>(null);
const [previewItem, setPreviewItem] = useState<string | null>(null);
const [fileItem, setFileItem] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: kontakState.update.form.nama || '',
imageId: kontakState.update.form.imageId || '',
kontakItem: {
nama: kontakState.update.form.kontakItems[0].nama || '',
nomorTelepon: kontakState.update.form.kontakItems[0].nomorTelepon || '',
imageId: kontakState.update.form.kontakItems[0].imageId || '',
}
})
useEffect(() => {
const loadKontakDarurat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await kontakState.update.load(id);
if (data) {
setFormData({
name: data.nama || '',
imageId: data.imageId || '',
kontakItem: {
nama: data.kontakItems[0].nama || '',
nomorTelepon: data.kontakItems[0].nomorTelepon || '',
imageId: data.kontakItems[0].imageId || '',
},
});
if (data?.image?.link) {
setPreviewUtama(data.image.link);
}
if (data?.kontakItems[0].image?.link) {
setPreviewItem(data.kontakItems[0].image.link);
}
}
} catch (error) {
console.error("Error loading kontak darurat:", error);
toast.error("Gagal memuat data kontak darurat");
}
};
loadKontakDarurat();
}, [params?.id]);
const handleSubmit = async () => {
try {
kontakState.update.form = {
...kontakState.update.form,
nama: formData.name,
imageId: formData.imageId,
kontakItems: [
{
nama: formData.kontakItem.nama,
nomorTelepon: formData.kontakItem.nomorTelepon,
imageId: formData.kontakItem.imageId,
},
],
}
if(fileUtama) {
const res = await ApiFetch.api.fileStorage.create.post({ file: fileUtama, name: fileUtama.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
kontakState.update.form.imageId = uploaded.id;
}
if(fileItem) {
const res = await ApiFetch.api.fileStorage.create.post({ file: fileItem, name: fileItem.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
kontakState.update.form.kontakItems[0].imageId = uploaded.id;
}
await kontakState.update.update();
toast.success("Kontak Darurat berhasil diperbarui!");
router.push("/admin/keamanan/kontak-darurat");
} catch (error) {
console.error("Error updating kontak darurat:", error);
toast.error("Terjadi kesalahan saat memperbarui kontak darurat");
}
}
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 Kontak Darurat</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Darurat</Text>}
placeholder='Masukkan nama Kategori Darurat'
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFileUtama(selectedFile);
setPreviewUtama(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewUtama && (
<Box mt="sm">
<Image
src={previewUtama}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kontak</Text>}
placeholder='Masukkan nama Kontak'
value={formData.kontakItem.nama}
onChange={(val) => {
setFormData({ ...formData, kontakItem: { ...formData.kontakItem, nama: val.target.value } });
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nomor Telepon Kontak</Text>}
placeholder='Masukkan nomor telepon Kontak'
value={formData.kontakItem.nomorTelepon}
onChange={(val) => {
setFormData({ ...formData, kontakItem: { ...formData.kontakItem, nomorTelepon: val.target.value } });
}}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFileItem(selectedFile);
setPreviewItem(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewItem && (
<Box mt="sm">
<Image
src={previewItem}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKontakDarurat;

View File

@@ -0,0 +1,117 @@
'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 { useRouter,useParams } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import { useProxy } from 'valtio/utils';
import kontakDaruratKeamananState from '../../../_state/keamanan/kontak-darurat-keamanan';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailKontakDarurat() {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const params = useParams()
const kontakState = useProxy(kontakDaruratKeamananState)
useShallowEffect(() => {
kontakState.findUnique.load(params?.id as string)
}, [])
const handleDelete = () => {
if (selectedId) {
kontakState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/keamanan/kontak-darurat")
}
}
if (!kontakState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
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']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Kontak Darurat</Text>
{kontakState.findUnique.data ? (
<Paper key={kontakState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Kontak Darurat</Text>
<Text fz={"lg"}>{kontakState.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={kontakState.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kontak</Text>
<Stack>
{kontakState.findUnique.data?.kontakItems.map((item, index) => (
<Box key={index}>
<Text fz={"lg"}>{item.nama}</Text>
<Text fz={"lg"}>{item.nomorTelepon}</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={item.image?.link} alt="gambar" />
</Box>
))}
</Stack>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (kontakState.findUnique.data) {
setSelectedId(kontakState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={kontakState.delete.loading || !kontakState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (kontakState.findUnique.data) {
router.push(`/admin/keamanan/kontak-darurat/${kontakState.findUnique.data.id}/edit`);
}
}}
disabled={!kontakState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus kontak darurat ini?'
/>
</Box>
);
}
export default DetailKontakDarurat;

View File

@@ -1,43 +1,236 @@
'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 ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../_com/keamananEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import kontakDaruratKeamananState from '../../../_state/keamanan/kontak-darurat-keamanan';
function CreateKontakDarurat() {
const kontakState = useProxy(kontakDaruratKeamananState)
const router = useRouter();
const [fileUtama, setFileUtama] = useState<File | null>(null);
const [previewUtama, setPreviewUtama] = useState<string | null>(null);
const [fileItem, setFileItem] = useState<File | null>(null);
const [previewItem, setPreviewItem] = useState<string | null>(null);
const resetForm = () => {
kontakState.create.form = {
nama: "",
imageId: "",
kontakItems: [
{
nama: "",
nomorTelepon: "",
imageId: "",
},
],
}
setPreviewUtama(null);
setFileUtama(null);
setPreviewItem(null);
setFileItem(null);
}
const handleSubmit = async () => {
if (!fileUtama) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
const res = await ApiFetch.api.fileStorage.create.post({
file: fileUtama,
name: fileUtama.name,
})
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal mengupload file");
}
kontakState.create.form.imageId = uploaded.id;
if (!fileItem) {
return toast.error("Pilih file gambar terlebih dahulu");
}
const resItem = await ApiFetch.api.fileStorage.create.post({
file: fileItem,
name: fileItem.name,
})
const uploadedItem = resItem.data?.data;
if (!uploadedItem?.id) {
return toast.error("Gagal mengupload file");
}
kontakState.create.form.kontakItems[0].imageId = uploadedItem.id;
await kontakState.create.create();
resetForm();
router.push('/admin/keamanan/kontak-darurat');
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Kontak Darurat</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kontak Darurat</Text>}
placeholder='Masukkan nama Kontak Darurat'
value={kontakState.create.form.nama}
onChange={(val) => {
kontakState.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Kategori Darurat</Text>}
placeholder='Masukkan nama Kategori Darurat'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Kontak Darurat</Text>
<KeamananEditor
showSubmit={false}
/>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFileUtama(selectedFile);
setPreviewUtama(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewUtama && (
<Box mt="sm">
<Image
src={previewUtama}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kontak</Text>}
placeholder='Masukkan nama Kontak'
value={kontakState.create.form.kontakItems[0].nama}
onChange={(val) => {
kontakState.create.form.kontakItems[0].nama = val.target.value;
}}
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nomor Telepon Kontak</Text>}
placeholder='Masukkan nomor telepon Kontak'
value={kontakState.create.form.kontakItems[0].nomorTelepon}
onChange={(val) => {
kontakState.create.form.kontakItems[0].nomorTelepon = val.target.value;
}}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFileItem(selectedFile);
setPreviewItem(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewItem && (
<Box mt="sm">
<Image
src={previewItem}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</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 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"}>Nomor 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/keamanan/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 potensi ini?"
/> */}
</Box>
);
}
export default DetailKontakDarurat;

View File

@@ -1,44 +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 { KeamananEditor } from '../../_com/keamananEditor';
function EditKontakDarurat() {
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 Kontak Darurat</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Kontak Darurat</Text>}
placeholder='Masukkan nama Kontak Darurat'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Kontak Darurat</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKontakDarurat;

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, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } 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 kontakDaruratKeamananState from '../../_state/keamanan/kontak-darurat-keamanan';
import { useShallowEffect } from '@mantine/hooks';
function KontakDaurat() {
return (
@@ -20,7 +23,20 @@ function KontakDaurat() {
}
function ListKontakDaurat() {
const kontakState = useProxy(kontakDaruratKeamananState)
const router = useRouter();
useShallowEffect(() => {
kontakState.findMany.load()
}, [])
if (!kontakState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -31,23 +47,25 @@ function ListKontakDaurat() {
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kontak Darurat</TableTh>
<TableTh>Nomor Kontak Darurat</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Nama Kategori Darurat</TableTh>
<TableTh>Nama Kontak</TableTh>
<TableTh>Nomor Kontak</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Kontak Darurat 1</TableTd>
<TableTd>0896232831883</TableTd>
<TableTd>Kontak Darurat 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/keamanan/kontak-darurat/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
{kontakState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.kontakItems[0].nama}</TableTd>
<TableTd>{item.kontakItems[0].nomorTelepon}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/kontak-darurat/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>

View File

@@ -0,0 +1,151 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { DateTimePicker } from '@mantine/dates';
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';
export type Status = "Selesai" | "Proses" | "Gagal";
function EditLaporanPublik() {
const stateLaporan = useProxy(laporanPublikState)
const router = useRouter();
const params = useParams()
const [formData, setFormData] = useState({
judul: stateLaporan.edit.form.judul || '',
lokasi: stateLaporan.edit.form.lokasi || '',
tanggalWaktu: stateLaporan.edit.form.tanggalWaktu || '',
status: stateLaporan.edit.form.status || '',
penanganan: stateLaporan.edit.form.penanganan || '',
kronologi: stateLaporan.edit.form.kronologi || '',
})
useEffect(() => {
const loadLaporanPublik = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateLaporan.edit.load(id);
if (data) {
setFormData({
judul: data.judul || '',
lokasi: data.lokasi || '',
tanggalWaktu: data.tanggalWaktu || '',
status: data.status || '',
penanganan: data.penanganan?.map((p: any) => p.deskripsi)[0] || '',
kronologi: data.kronologi || '',
});
}
} catch (error) {
console.error('Error loading laporan publik:', error);
}
}
loadLaporanPublik();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateLaporan.edit.form = {
...stateLaporan.edit.form,
judul: formData.judul,
lokasi: formData.lokasi,
tanggalWaktu: formData.tanggalWaktu,
status: formData.status,
penanganan: formData.penanganan,
kronologi: formData.kronologi,
}
await stateLaporan.edit.update();
toast.success("Laporan Publik berhasil diperbarui!");
router.push("/admin/keamanan/laporan-publik");
} catch (error) {
console.error("Error updating kontak darurat:", error);
toast.error("Terjadi kesalahan saat memperbarui kontak darurat");
}
}
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 Laporan Publik</Title>
<TextInput
value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Judul Laporan Publik</Text>}
placeholder='Masukkan judul LaporanPublik'
/>
<TextInput
value={formData.lokasi}
onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Lokasi Laporan Publik</Text>}
placeholder='Masukkan lokasi LaporanPublik'
/>
<DateTimePicker
label="Tanggal Laporan Publik"
value={
formData.tanggalWaktu
? new Date(formData.tanggalWaktu)
: null
}
onChange={(val) => {
if (val) {
setFormData({ ...formData, tanggalWaktu: val.toString() });
} else {
setFormData({ ...formData, tanggalWaktu: "" }); // Reset kalau dikosongkan
}
}}
/>
<Select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e?.valueOf() as Status })}
label={<Text fw={"bold"} fz={"sm"}>Status Laporan Publik</Text>}
placeholder='Masukkan status LaporanPublik'
data={[
{ value: "Selesai", label: "Selesai" },
{ value: "Proses", label: "Proses" },
{ value: "Gagal", label: "Gagal" },
]}
/>
<TextInput
value={formData.kronologi}
onChange={(e) => setFormData({ ...formData, kronologi: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Kronologi Laporan Publik</Text>}
placeholder='Masukkan kronologi LaporanPublik'
/>
<Text fw={"bold"} fz={"sm"}>Penanganan Laporan Publik</Text>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Laporan Publik</Text>
<EditEditor
value={formData.penanganan}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, penanganan: htmlContent }));
stateLaporan.edit.form.penanganan = htmlContent;
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditLaporanPublik;

View File

@@ -0,0 +1,128 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import laporanPublikState from '../../../_state/keamanan/laporan-publik';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailLaporanPublik() {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const stateLaporan = useProxy(laporanPublikState)
const params = useParams()
const router = useRouter();
useShallowEffect(() => {
stateLaporan.findUnique.load(params?.id as string)
}, [])
const handleDelete = () => {
if (selectedId) {
stateLaporan.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/keamanan/laporan-publik")
}
}
if (!stateLaporan.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 Laporan Publik</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul Laporan Publik</Text>
<Text fz={"lg"}>{stateLaporan.findUnique.data?.judul}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal Laporan Publik</Text>
<Text fz={"lg"}>
{stateLaporan.findUnique.data?.tanggalWaktu
? new Date(stateLaporan.findUnique.data.tanggalWaktu).toLocaleString('id-ID')
: '-'}
</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Lokasi</Text>
<Text fz={"lg"}>{stateLaporan.findUnique.data?.lokasi}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Status</Text>
<Text fz={"lg"}>{stateLaporan.findUnique.data?.status}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kronologi</Text>
<Text fz={"lg"}>
{stateLaporan.findUnique.data?.kronologi || '-'}
</Text>
</Box>
<Text fz={"lg"} fw={"bold"}>Penanganan</Text>
{stateLaporan.findUnique.data?.penanganan?.map((item, index) => (
<Box key={index}>
<Text fz={"lg"} fw={"bold"}>Deskripsi Penanganan</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
))}
{!stateLaporan.findUnique.data?.penanganan?.length && (
<Text fz={"lg"} fs="italic">Belum ada penanganan</Text>
)}
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateLaporan.findUnique.data) {
setSelectedId(stateLaporan.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateLaporan.delete.loading || !stateLaporan.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateLaporan.findUnique.data) {
router.push(`/admin/keamanan/laporan-publik/${stateLaporan.findUnique.data.id}/edit`);
}
}}
disabled={!stateLaporan.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus laporan publik ini?'
/>
</Stack>
</Paper>
</Box>
);
}
export default DetailLaporanPublik;

View File

@@ -1,47 +1,105 @@
'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 { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { DateTimePicker } from '@mantine/dates';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../_com/keamananEditor';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import laporanPublikState from '../../../_state/keamanan/laporan-publik';
export type Status = "Selesai" | "Proses" | "Gagal";
function CreateLaporanPublik() {
const stateLaporan = useProxy(laporanPublikState)
const router = useRouter();
const resetForm = () => {
stateLaporan.create.form = {
judul: "",
lokasi: "",
tanggalWaktu: "",
status: "Proses" as Status,
penanganan: "",
kronologi: "",
}
}
const handleSubmit = async () => {
await stateLaporan.create.create();
resetForm();
router.push('/admin/keamanan/laporan-publik');
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Laporan Publik</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Laporan Publik</Text>}
placeholder='Masukkan judul LaporanPublik'
value={stateLaporan.create.form.judul}
onChange={(e) => stateLaporan.create.form.judul = e.target.value}
label={<Text fw={"bold"} fz={"sm"}>Judul Laporan Publik</Text>}
placeholder='Masukkan judul LaporanPublik'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Tanggal Laporan Publik</Text>}
placeholder='Masukkan tanggal LaporanPublik'
value={stateLaporan.create.form.lokasi}
onChange={(e) => stateLaporan.create.form.lokasi = e.target.value}
label={<Text fw={"bold"} fz={"sm"}>Lokasi Laporan Publik</Text>}
placeholder='Masukkan lokasi LaporanPublik'
/>
<DateTimePicker
label="Tanggal Laporan Publik"
value={
stateLaporan.create.form.tanggalWaktu
? new Date(stateLaporan.create.form.tanggalWaktu)
: null
}
onChange={(val) => {
if (val) {
stateLaporan.create.form.tanggalWaktu = val.toString();
} else {
stateLaporan.create.form.tanggalWaktu = ""; // Reset kalau dikosongkan
}
}}
/>
<Select
value={stateLaporan.create.form.status}
onChange={(e) => stateLaporan.create.form.status = e?.valueOf() as Status}
label={<Text fw={"bold"} fz={"sm"}>Status Laporan Publik</Text>}
placeholder='Masukkan status LaporanPublik'
data={[
{ value: "Selesai", label: "Selesai" },
{ value: "Proses", label: "Proses" },
{ value: "Gagal", label: "Gagal" },
]}
/>
<TextInput
value={stateLaporan.create.form.kronologi}
onChange={(e) => stateLaporan.create.form.kronologi = e.target.value}
label={<Text fw={"bold"} fz={"sm"}>Kronologi Laporan Publik</Text>}
placeholder='Masukkan kronologi LaporanPublik'
/>
<Text fw={"bold"} fz={"sm"}>Penanganan Laporan Publik</Text>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Laporan Publik</Text>
<KeamananEditor
showSubmit={false}
<CreateEditor
value={stateLaporan.create.form.penanganan}
onChange={(e) => stateLaporan.create.form.penanganan = e}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</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 DetailLaporanPublik() {
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 Laporan Publik</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul Laporan Publik</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal Laporan Publik</Text>
<Text fz={"lg"}>Test Tanggal</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/keamanan/laporan-publik/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 DetailLaporanPublik;

View File

@@ -1,48 +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 { KeamananEditor } from '../../_com/keamananEditor';
function EditLaporanPublik() {
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 Laporan Publik</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul Laporan Publik</Text>}
placeholder='Masukkan judul Laporan Publik'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Tanggal Laporan Publik</Text>}
placeholder='Masukkan tanggal Laporan Publik'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Laporan Publik</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditLaporanPublik;

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, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } 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 laporanPublikState from '../../_state/keamanan/laporan-publik';
import { useShallowEffect } from '@mantine/hooks';
function LaporanPublik() {
return (
@@ -20,7 +23,20 @@ function LaporanPublik() {
}
function ListLaporanPublik() {
const stateLaporan = useProxy(laporanPublikState)
const router = useRouter();
useShallowEffect(() => {
stateLaporan.findMany.load()
}, [])
if (!stateLaporan.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -33,21 +49,25 @@ function ListLaporanPublik() {
<TableTr>
<TableTh>Judul Laporan Publik</TableTh>
<TableTh>Tanggal Laporan Publik</TableTh>
<TableTh>Status</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Laporan Publik 1</TableTd>
<TableTd>0896232831883</TableTd>
<TableTd>Laporan Publik 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/keamanan/laporan-publik/detail')}>
{stateLaporan.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.judul}</TableTd>
<TableTd>{new Date(item.tanggalWaktu).toLocaleDateString('id-ID')}</TableTd>
<TableTd>{item.status}</TableTd>
<TableTd>{item.kronologi}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/laporan-publik/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>

View File

@@ -0,0 +1,214 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import colors from '@/con/colors';
import { Box, Button, 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 EditPencegahanKriminalitas() {
const router = useRouter();
const params = useParams()
const kriminalitasState = useProxy(pencegahanKriminalitasState)
const [formData, setFormData] = useState({
pencegahanKriminalitas: {
programKeamanan: {
nama: kriminalitasState.update.form.pencegahanKriminalitas.programKeamanan.nama,
deskripsi: kriminalitasState.update.form.pencegahanKriminalitas.programKeamanan.deskripsi,
slug: kriminalitasState.update.form.pencegahanKriminalitas.programKeamanan.slug,
},
tipsKeamanan: {
judul: kriminalitasState.update.form.pencegahanKriminalitas.tipsKeamanan.judul,
konten: kriminalitasState.update.form.pencegahanKriminalitas.tipsKeamanan.konten,
slug: kriminalitasState.update.form.pencegahanKriminalitas.tipsKeamanan.slug,
},
videoKeamanan: {
judul: kriminalitasState.update.form.pencegahanKriminalitas.videoKeamanan.judul,
deskripsi: kriminalitasState.update.form.pencegahanKriminalitas.videoKeamanan.deskripsi,
videoUrl: kriminalitasState.update.form.pencegahanKriminalitas.videoKeamanan.videoUrl,
slug: kriminalitasState.update.form.pencegahanKriminalitas.videoKeamanan.slug,
},
},
})
useEffect(() => {
const loadKriminalitas = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await kriminalitasState.update.load(id);
if (data && data.pencegahanKriminalitas) {
const { programKeamanan, tipsKeamanan, videoKeamanan } = data.pencegahanKriminalitas;
setFormData({
pencegahanKriminalitas: {
programKeamanan: {
nama: programKeamanan?.nama || "",
deskripsi: programKeamanan?.deskripsi || "",
slug: programKeamanan?.slug || "",
},
tipsKeamanan: {
judul: tipsKeamanan?.judul || "",
konten: tipsKeamanan?.konten || "",
slug: tipsKeamanan?.slug || "",
},
videoKeamanan: {
judul: videoKeamanan?.judul || "",
deskripsi: videoKeamanan?.deskripsi || "",
videoUrl: videoKeamanan?.videoUrl || "",
slug: videoKeamanan?.slug || "",
},
},
});
}
} catch (error) {
console.error("Error loading pencegahan kriminalitas:", error);
toast.error("Gagal memuat data pencegahan kriminalitas");
}
}
loadKriminalitas();
}, [params.id]);
const handleSubmit = async () => {
try {
kriminalitasState.update.form = {
...kriminalitasState.update.form,
pencegahanKriminalitas: {
programKeamanan: {
nama: formData.pencegahanKriminalitas.programKeamanan.nama,
deskripsi: formData.pencegahanKriminalitas.programKeamanan.deskripsi,
slug: formData.pencegahanKriminalitas.programKeamanan.slug,
},
tipsKeamanan: {
judul: formData.pencegahanKriminalitas.tipsKeamanan.judul,
konten: formData.pencegahanKriminalitas.tipsKeamanan.konten,
slug: formData.pencegahanKriminalitas.tipsKeamanan.slug,
},
videoKeamanan: {
judul: formData.pencegahanKriminalitas.videoKeamanan.judul,
deskripsi: formData.pencegahanKriminalitas.videoKeamanan.deskripsi,
videoUrl: formData.pencegahanKriminalitas.videoKeamanan.videoUrl,
slug: formData.pencegahanKriminalitas.videoKeamanan.slug,
},
},
}
await kriminalitasState.update.update();
toast.success("Pencegahan Kriminalitas berhasil diperbarui!");
router.push("/admin/keamanan/pencegahan-kriminalitas");
} catch (error) {
console.error("Error updating pencegahan kriminalitas:", error);
toast.error("Gagal memuat data pencegahan kriminalitas");
}
}
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 Pencegahan Kriminalitas</Title>
<TextInput
value={formData.pencegahanKriminalitas.programKeamanan.nama}
onChange={(val) => {
formData.pencegahanKriminalitas.programKeamanan.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Program Keamanan</Text>}
placeholder='Masukkan judul Program Keamanan'
/>
<TextInput
value={formData.pencegahanKriminalitas.programKeamanan.slug}
onChange={(val) => {
formData.pencegahanKriminalitas.programKeamanan.slug = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Slug Program Keamanan</Text>}
placeholder='Masukkan slug Program Keamanan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Keamanan</Text>
<EditEditor
value={formData.pencegahanKriminalitas.programKeamanan.deskripsi}
onChange={(val) => {
formData.pencegahanKriminalitas.programKeamanan.deskripsi = val;
}}
/>
</Box>
<TextInput
value={formData.pencegahanKriminalitas.tipsKeamanan.judul}
onChange={(val) => {
formData.pencegahanKriminalitas.tipsKeamanan.judul = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Tips Keamanan</Text>}
placeholder='Masukkan judul Tips Keamanan'
/>
<TextInput
value={formData.pencegahanKriminalitas.tipsKeamanan.slug}
onChange={(val) => {
formData.pencegahanKriminalitas.tipsKeamanan.slug = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Slug Tips Keamanan</Text>}
placeholder='Masukkan slug Tips Keamanan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
<EditEditor
value={formData.pencegahanKriminalitas.tipsKeamanan.konten}
onChange={(val) => {
formData.pencegahanKriminalitas.tipsKeamanan.konten = val;
}}
/>
</Box>
<TextInput
value={formData.pencegahanKriminalitas.videoKeamanan.judul}
onChange={(val) => {
formData.pencegahanKriminalitas.videoKeamanan.judul = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Video Keamanan</Text>}
placeholder='Masukkan judul Video Keamanan'
/>
<TextInput
value={formData.pencegahanKriminalitas.videoKeamanan.slug}
onChange={(val) => {
formData.pencegahanKriminalitas.videoKeamanan.slug = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Slug Video Keamanan</Text>}
placeholder='Masukkan slug Video Keamanan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
<EditEditor
value={formData.pencegahanKriminalitas.videoKeamanan.deskripsi}
onChange={(val) => {
formData.pencegahanKriminalitas.videoKeamanan.deskripsi = val;
}}
/>
</Box>
<TextInput
value={formData.pencegahanKriminalitas.videoKeamanan.videoUrl}
onChange={(val) => {
formData.pencegahanKriminalitas.videoKeamanan.videoUrl = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Video URL</Text>}
placeholder='Masukkan video URL'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPencegahanKriminalitas;

View File

@@ -0,0 +1,121 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas';
function DetailPencegahanKriminalitas() {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const params = useParams()
const kriminalitasState = useProxy(pencegahanKriminalitasState)
useShallowEffect(() => {
kriminalitasState.findUnique.load(params?.id as string)
}, [])
const handleDelete = () => {
if (selectedId) {
kriminalitasState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/keamanan/pencegahan-kriminalitas")
}
}
if (!kriminalitasState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</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>
<Paper bg={colors['BG-trans']} p={'md'}>
<Text fz={"xl"} fw={"bold"}>Detail Pencegahan Kriminalitas</Text>
{kriminalitasState.findUnique.data ? (
<Paper key={kriminalitasState.findUnique.data.id} bg={colors['BG-trans']}>
<Stack gap={"xs"} py={'md'}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Program Keamanan</Text>
<Text fz={"lg"}>{kriminalitasState.findUnique.data?.programKeamanan.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Slug</Text>
<Text fz={"lg"}>{kriminalitasState.findUnique.data?.programKeamanan.slug}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kriminalitasState.findUnique.data?.programKeamanan.deskripsi || '' }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Tips Keamanan</Text>
<Text fz={"lg"}>{kriminalitasState.findUnique.data?.tipsKeamanan.judul}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Slug Tips Keamanan</Text>
<Text fz={"lg"}>{kriminalitasState.findUnique.data?.tipsKeamanan.slug}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi Tips Keamanan</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kriminalitasState.findUnique.data?.tipsKeamanan.konten || '' }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (kriminalitasState.findUnique.data) {
setSelectedId(kriminalitasState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={kriminalitasState.delete.loading || !kriminalitasState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (kriminalitasState.findUnique.data) {
router.push(`/admin/keamanan/pencegahan-kriminalitas/${kriminalitasState.findUnique.data.id}/edit`);
}
}}
disabled={!kriminalitasState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus pencegahan kriminalitas ini?"
/>
</Box>
);
}
export default DetailPencegahanKriminalitas;

View File

@@ -1,43 +1,145 @@
'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 { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../_com/keamananEditor';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas';
function CreatePencegahanKriminalitas() {
const router = useRouter();
const kriminalitasState = useProxy(pencegahanKriminalitasState)
const resetForm = () => {
kriminalitasState.create.form = {
pencegahanKriminalitas: {
programKeamanan: {
nama: "",
deskripsi: "",
slug: "",
},
tipsKeamanan: {
judul: "",
konten: "",
slug: "",
},
videoKeamanan: {
judul: "",
deskripsi: "",
videoUrl: "",
slug: "",
},
},
}
}
const handleSubmit = async () => {
await kriminalitasState.create.create();
resetForm();
router.push('/admin/keamanan/pencegahan-kriminalitas');
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Pencegahan Kriminalitas</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Pencegahan Kriminalitas</Text>}
placeholder='Masukkan nama Pencegahan Kriminalitas'
value={kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.nama}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Program Keamanan</Text>}
placeholder='Masukkan judul Program Keamanan'
/>
<TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.slug}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.slug = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Slug Program Keamanan</Text>}
placeholder='Masukkan slug Program Keamanan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Pencegahan Kriminalitas</Text>
<KeamananEditor
showSubmit={false}
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Keamanan</Text>
<CreateEditor
value={kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.deskripsi}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.programKeamanan.deskripsi = val;
}}
/>
</Box>
<TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.judul}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.judul = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Tips Keamanan</Text>}
placeholder='Masukkan judul Tips Keamanan'
/>
<TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.slug}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.slug = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Slug Tips Keamanan</Text>}
placeholder='Masukkan slug Tips Keamanan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
<CreateEditor
value={kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.konten}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.tipsKeamanan.konten = val;
}}
/>
</Box>
<TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.judul}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.judul = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Judul Video Keamanan</Text>}
placeholder='Masukkan judul Video Keamanan'
/>
<TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.slug}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.slug = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Slug Video Keamanan</Text>}
placeholder='Masukkan slug Video Keamanan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
<CreateEditor
value={kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.deskripsi}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.deskripsi = val;
}}
/>
</Box>
<TextInput
value={kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.videoUrl}
onChange={(val) => {
kriminalitasState.create.form.pencegahanKriminalitas.videoKeamanan.videoUrl = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Video URL</Text>}
placeholder='Masukkan video URL'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</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 DetailPencegahanKriminalitas() {
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 Pencegahan Kriminalitas</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Pencegahan Kriminalitas</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nomor Pencegahan Kriminalitas</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/keamanan/pencegahan-kriminalitas/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 DetailPencegahanKriminalitas;

View File

@@ -1,44 +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 { KeamananEditor } from '../../_com/keamananEditor';
function EditPencegahanKriminalitas() {
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 Pencegahan Kriminalitas</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Pencegahan Kriminalitas</Text>}
placeholder='Masukkan nama Pencegahan Kriminalitas'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Pencegahan Kriminalitas</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPencegahanKriminalitas;

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 pencegahanKriminalitasState from '../../_state/keamanan/pencegahan-kriminalitas';
import { useShallowEffect } from '@mantine/hooks';
function PencegahanKriminalitas() {
return (
@@ -20,34 +23,52 @@ function PencegahanKriminalitas() {
}
function ListPencegahanKriminalitas() {
const kriminalitasState = useProxy(pencegahanKriminalitasState)
const router = useRouter();
useShallowEffect(() => {
kriminalitasState.findMany.load()
}, [])
if (!kriminalitasState.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500}/>
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pencegahan Kriminalitas'
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pencegahan Kriminalitas'
href='/admin/keamanan/pencegahan-kriminalitas/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Pencegahan Kriminalitas</TableTh>
<TableTh>Nomor Pencegahan Kriminalitas</TableTh>
<TableTh>Slug</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Pencegahan Kriminalitas 1</TableTd>
<TableTd>0896232831883</TableTd>
<TableTd>Pencegahan Kriminalitas 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/keamanan/pencegahan-kriminalitas/detail')}>
{kriminalitasState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.programKeamanan.nama}</TableTd>
<TableTd>{item.programKeamanan.slug}</TableTd>
<TableTd>
<Text fz={'sm'} dangerouslySetInnerHTML={{__html: item.programKeamanan.deskripsi || ''}} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/pencegahan-kriminalitas/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>

View File

@@ -0,0 +1,372 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Card, Group, Modal, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import polsekTerdekat from '@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat';
function CreatePolsekTerdekat() {
const polsekState = useProxy(polsekTerdekat)
const params = useParams();
const router = useRouter();
const [layananOptions, setLayananOptions] = useState<{ value: string; label: string }[]>([]);
const [modalOpen, setModalOpen] = useState(false);
const [modalUpdateOpen, setModalUpdateOpen] = useState(false);
const [namaLayananBaru, setNamaLayananBaru] = useState("");
const [selectedLayananId, setSelectedLayananId] = useState<string | null>(null);
const [namaLayananUpdate, setNamaLayananUpdate] = useState("");
const [formData, setFormData] = useState({
nama: "",
jarakKeDesa: "",
alamat: "",
nomorTelepon: "",
jamOperasional: "",
embedMapUrl: "",
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: "",
})
useEffect(() => {
const loadPolsekTerdekat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await polsekState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || '',
jarakKeDesa: data.jarakKeDesa || '',
alamat: data.alamat || '',
nomorTelepon: data.nomorTelepon || '',
jamOperasional: data.jamOperasional || '',
embedMapUrl: data.embedMapUrl || '',
namaTempatMaps: data.namaTempatMaps || '',
alamatMaps: data.alamatMaps || '',
linkPetunjukArah: data.linkPetunjukArah || '',
layananPolsekId: data.layananPolsekId || '',
});
}
} catch (error) {
console.error("Error loading polsek terdekat:", error);
toast.error("Gagal memuat data polsek terdekat");
}
};
loadPolsekTerdekat();
}, [params?.id]);
const handleSubmit = async () => {
try {
polsekState.edit.form = {
...polsekState.edit.form,
nama: formData.nama,
jarakKeDesa: formData.jarakKeDesa,
alamat: formData.alamat,
nomorTelepon: formData.nomorTelepon,
jamOperasional: formData.jamOperasional,
embedMapUrl: formData.embedMapUrl,
namaTempatMaps: formData.namaTempatMaps,
alamatMaps: formData.alamatMaps,
linkPetunjukArah: formData.linkPetunjukArah,
layananPolsekId: formData.layananPolsekId,
}
await polsekState.edit.update()
toast.success("Polsek terdekat berhasil diperbarui!")
router.push("/admin/keamanan/polsek-terdekat")
} catch (error) {
console.error("Error updating polsek terdekat:", error);
toast.error("Gagal memuat data polsek terdekat");
}
}
const fetchLayanan = async () => {
try {
const res = await fetch("/api/keamanan/layanan-polsek/find-many")
const data = await res.json()
if (data.success) {
const options = data.data.map((item: any) => {
return {
value: item.id,
label: item.nama
}
})
setLayananOptions(options)
}
} catch {
toast.error("Gagal memuat layanan polsek")
}
}
const handleTambahLayanan = async () => {
if (!namaLayananBaru.trim()) return toast.warn("Nama layanan tidak boleh kosong");
try {
const res = await fetch("/api/keamanan/layanan-polsek/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nama: namaLayananBaru }),
});
const data = await res.json();
console.log("data setelah create:", data);
if (data.success) {
const newLayanan = {
value: data.data.id,
label: data.data.nama,
};
setLayananOptions((prev) => [...prev, newLayanan]);
await fetchLayanan();
polsekState.create.form.layananPolsekId = data.data.id;
toast.success("Layanan baru ditambahkan!");
setModalOpen(false);
setNamaLayananBaru("");
} else {
toast.error(data.message || "Gagal menambah layanan");
}
} catch {
toast.error("Error menambah layanan");
}
};
const handleUpdateLayanan = async (id: string, namaBaru: string) => {
if (!namaBaru.trim()) return toast.warn("Nama layanan tidak boleh kosong");
try {
const res = await fetch(`/api/keamanan/layanan-polsek/update/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nama: namaBaru }),
});
const data = await res.json();
console.log("data setelah update:", data);
if (data.success) {
await fetchLayanan(); // Refresh list
toast.success("Layanan berhasil diupdate!");
setModalUpdateOpen(false);
setNamaLayananUpdate("");
} else {
toast.error(data.message || "Gagal mengupdate layanan");
}
} catch {
toast.error("Error mengupdate layanan");
}
};
const handleDeleteLayanan = async (id: string) => {
const confirmDelete = confirm("Yakin ingin menghapus layanan ini?");
if (!confirmDelete) return;
try {
const res = await fetch(`/api/keamanan/layanan-polsek/del/${id}`, {
method: "DELETE",
});
const data = await res.json();
console.log("data setelah delete:", data);
if (data.success) {
await fetchLayanan(); // Refresh list
setLayananOptions((prev) => prev.filter((layanan) => layanan.value !== id));
toast.success("Layanan berhasil dihapus!");
} else {
toast.error(data.message || "Gagal menghapus layanan");
}
} catch {
toast.error("Error menghapus layanan");
}
};
useEffect(() => {
fetchLayanan();
}, []);
return (
<Box>
<Modal
opened={modalOpen}
onClose={() => setModalOpen(false)}
title="Tambah Layanan Polsek"
centered
>
<Stack>
<TextInput
label="Nama Layanan"
placeholder="Masukkan nama layanan"
value={namaLayananBaru}
onChange={(e) => setNamaLayananBaru(e.currentTarget.value)}
/>
<Button onClick={handleTambahLayanan}>Simpan</Button>
</Stack>
</Modal>
<Modal
opened={modalUpdateOpen}
onClose={() => setModalUpdateOpen(false)}
title="Tambah Layanan Polsek"
centered
>
<Stack>
<TextInput
label="Nama Layanan"
placeholder="Masukkan nama layanan"
value={namaLayananUpdate}
onChange={(e) => setNamaLayananUpdate(e.currentTarget.value)}
/>
<Button onClick={() => {
if (!selectedLayananId) return toast.warn("ID layanan tidak ditemukan");
handleUpdateLayanan(selectedLayananId, namaLayananUpdate);
}}>Simpan</Button>
</Stack>
</Modal>
<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}>Create Polsek Terdekat</Title>
<TextInput
value={formData.nama}
onChange={(val) => {
formData.nama = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Polsek Terdekat</Text>}
placeholder='Masukkan nama Polsek Terdekat'
/>
<TextInput
value={formData.jarakKeDesa}
onChange={(val) => {
formData.jarakKeDesa = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Jarak Polsek Terdekat</Text>}
placeholder='Masukkan jarak Polsek Terdekat'
/>
<TextInput
value={formData.alamat}
onChange={(val) => {
formData.alamat = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Alamat Polsek Terdekat</Text>}
placeholder='Masukkan alamat Polsek Terdekat'
/>
<TextInput
value={formData.nomorTelepon}
onChange={(val) => {
formData.nomorTelepon = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Nomor Telepon Polsek Terdekat</Text>}
placeholder='Masukkan nomor telepon Polsek Terdekat'
/>
<TextInput
value={formData.jamOperasional}
onChange={(val) => {
formData.jamOperasional = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Jam Operasional Polsek Terdekat</Text>}
placeholder='Masukkan jam operasional Polsek Terdekat'
/>
<TextInput
value={formData.embedMapUrl}
onChange={(val) => {
formData.embedMapUrl = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Embed Map URL Polsek Terdekat</Text>}
placeholder='Masukkan embed map url Polsek Terdekat'
/>
<TextInput
value={formData.namaTempatMaps}
onChange={(val) => {
formData.namaTempatMaps = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Tempat Maps Polsek Terdekat</Text>}
placeholder='Masukkan nama tempat maps Polsek Terdekat'
/>
<TextInput
value={formData.alamatMaps}
onChange={(val) => {
formData.alamatMaps = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Alamat Maps Polsek Terdekat</Text>}
placeholder='Masukkan alamat maps Polsek Terdekat'
/>
<TextInput
value={formData.linkPetunjukArah}
onChange={(val) => {
formData.linkPetunjukArah = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Link Petunjuk Arah Polsek Terdekat</Text>}
placeholder='Masukkan link petunjuk arah Polsek Terdekat'
/>
{/* Tambah Dropdown Select */}
<Select
label="Layanan Polsek"
placeholder="Pilih layanan polsek"
data={layananOptions}
value={polsekState.create.form.layananPolsekId}
onChange={(val) => {
polsekState.create.form.layananPolsekId = val || "";
}}
/>
<Button variant="light" size="xs" onClick={() => setModalOpen(true)}>
+ Tambah Layanan Baru
</Button>
<Text>Daftar Layanan Polsek</Text>
{layananOptions.map((item) => (
<Card style={{border: '1px solid #ccc'}} bg={colors['white-1']} p={'md'} key={item.value}>
<Group>
<Text>{item.label}</Text>
<Button
variant="light"
size="xs"
onClick={() => {
setSelectedLayananId(item.value);
setNamaLayananUpdate(item.label);
setModalUpdateOpen(true);
}}
>
Edit
</Button>
<Button
variant="outline"
color="red"
size="xs"
onClick={() => handleDeleteLayanan(item.value)}
>
Hapus
</Button>
</Group>
</Card>
))}
{/* Tambah field lainnya di sini... */}
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box >
);
}
export default CreatePolsekTerdekat;

View File

@@ -0,0 +1,157 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import polsekTerdekat from '../../../_state/keamanan/polsek-terdekat';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPolsekTerdekat() {
const router = useRouter();
const polsekState = useProxy(polsekTerdekat)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [modalHapus, setModalHapus] = useState(false)
const params = useParams()
useShallowEffect(() => {
polsekState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
polsekState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/keamanan/polsek-terdekat")
}
}
if (!polsekState.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 Polsek Terdekat</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Polsek Terdekat</Text>
<Text fz={"lg"}>{polsekState.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jarak Polsek Ke Desa</Text>
<Text fz={"lg"}>{polsekState.findUnique.data?.jarakKeDesa}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat Polsek </Text>
<Text fz={"lg"}>{polsekState.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nomor Polsek</Text>
<Text fz={"lg"}>{polsekState.findUnique.data?.nomorTelepon}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jam Operasional</Text>
<Text fz={"lg"}>{polsekState.findUnique.data?.jamOperasional}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Google Maps</Text>
{polsekState.findUnique.data?.embedMapUrl ? (
<Box style={{ position: 'relative', paddingBottom: '56.25%', height: 0, overflow: 'hidden' }}>
<iframe
src={polsekState.findUnique.data.embedMapUrl}
width="100%"
height="100%"
style={{
border: 0,
position: 'absolute',
top: 0,
left: 0,
}}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
/>
</Box>
) : (
<Text c="dimmed" fz="sm">Tidak ada maps</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Tempat Maps</Text>
<Text fz={"lg"}>{polsekState.findUnique.data?.namaTempatMaps}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat Maps</Text>
<Text fz={"lg"}>{polsekState.findUnique.data?.alamatMaps}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Link Petunjuk Arah</Text>
<Text>
<a
href={polsekState.findUnique.data?.linkPetunjukArah || "#"}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'black', textDecoration: 'underline' }}
>
{polsekState.findUnique.data?.linkPetunjukArah || "Tidak ada link"}
</a>
</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Layanan Polsek</Text>
<Text fz={"lg"}>{polsekState.findUnique.data?.layananPolsek?.nama}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (polsekState.findUnique.data) {
setSelectedId(polsekState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={polsekState.delete.loading || !polsekState.findUnique.data}
color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/keamanan/polsek-terdekat/${polsekState.findUnique.data?.id}/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 polsek terdekat ini?"
/>
</Box>
);
}
export default DetailPolsekTerdekat;

View File

@@ -0,0 +1,9 @@
function convertGoogleMapsToEmbed(url: string): string | null {
const match = url.match(/https:\/\/www\.google\.com\/maps\/place\/([^/]+)/);
if (!match) return null;
const place = match[1]; // misal: Polsek+Denpasar+Selatan
return `https://www.google.com/maps/embed/v1/place?key=YOUR_API_KEY&q=${place}`;
}
export default convertGoogleMapsToEmbed

View File

@@ -1,14 +1,116 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'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 { Box, Button, Group, Modal, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../_com/keamananEditor';
import { useProxy } from 'valtio/utils';
import polsekTerdekat from '../../../_state/keamanan/polsek-terdekat';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
function CreatePolsekTerdekat() {
const polsekState = useProxy(polsekTerdekat)
const router = useRouter();
const [layananOptions, setLayananOptions] = useState<{ value: string; label: string }[]>([]);
const [modalOpen, setModalOpen] = useState(false);
const [namaLayananBaru, setNamaLayananBaru] = useState("");
const resetForm = () => {
polsekState.create.form = {
nama: "",
jarakKeDesa: "",
alamat: "",
nomorTelepon: "",
jamOperasional: "",
embedMapUrl: "",
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: "",
}
}
const handleSubmit = async () => {
await polsekState.create.create()
resetForm()
router.push("/admin/keamanan/polsek-terdekat")
}
const fetchLayanan = async () => {
try {
const res = await fetch("/api/keamanan/layanan-polsek/find-many")
const data = await res.json()
if (data.success) {
const options = data.data.map((item: any) => {
return {
value: item.id,
label: item.nama
}
})
setLayananOptions(options)
}
} catch {
toast.error("Gagal memuat layanan polsek")
}
}
const handleTambahLayanan = async () => {
if (!namaLayananBaru.trim()) return toast.warn("Nama layanan tidak boleh kosong");
try {
const res = await fetch("/api/keamanan/layanan-polsek/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nama: namaLayananBaru }),
});
const data = await res.json();
console.log("data setelah create:", data);
if (data.success) {
const newLayanan = {
value: data.data.id,
label: data.data.nama,
};
setLayananOptions((prev) => [...prev, newLayanan]);
await fetchLayanan();
polsekState.create.form.layananPolsekId = data.data.id;
toast.success("Layanan baru ditambahkan!");
setModalOpen(false);
setNamaLayananBaru("");
} else {
toast.error(data.message || "Gagal menambah layanan");
}
} catch {
toast.error("Error menambah layanan");
}
};
useEffect(() => {
fetchLayanan();
}, []);
return (
<Box>
<Modal
opened={modalOpen}
onClose={() => setModalOpen(false)}
title="Tambah Layanan Polsek"
centered
>
<Stack>
<TextInput
label="Nama Layanan"
placeholder="Masukkan nama layanan"
value={namaLayananBaru}
onChange={(e) => setNamaLayananBaru(e.currentTarget.value)}
/>
<Button onClick={handleTambahLayanan}>Simpan</Button>
</Stack>
</Modal>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
@@ -18,27 +120,103 @@ function CreatePolsekTerdekat() {
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Polsek Terdekat</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
value={polsekState.create.form.nama}
onChange={(val) => {
polsekState.create.form.nama = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Polsek Terdekat</Text>}
placeholder='Masukkan nama Polsek Terdekat'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Polsek Terdekat</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<TextInput
value={polsekState.create.form.jarakKeDesa}
onChange={(val) => {
polsekState.create.form.jarakKeDesa = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Jarak Polsek Terdekat</Text>}
placeholder='Masukkan jarak Polsek Terdekat'
/>
<TextInput
value={polsekState.create.form.alamat}
onChange={(val) => {
polsekState.create.form.alamat = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Alamat Polsek Terdekat</Text>}
placeholder='Masukkan alamat Polsek Terdekat'
/>
<TextInput
value={polsekState.create.form.nomorTelepon}
onChange={(val) => {
polsekState.create.form.nomorTelepon = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Nomor Telepon Polsek Terdekat</Text>}
placeholder='Masukkan nomor telepon Polsek Terdekat'
/>
<TextInput
value={polsekState.create.form.jamOperasional}
onChange={(val) => {
polsekState.create.form.jamOperasional = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Jam Operasional Polsek Terdekat</Text>}
placeholder='Masukkan jam operasional Polsek Terdekat'
/>
<TextInput
value={polsekState.create.form.embedMapUrl}
onChange={(val) => {
polsekState.create.form.embedMapUrl = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Embed Map URL Polsek Terdekat</Text>}
placeholder='Masukkan embed map url Polsek Terdekat'
/>
<TextInput
value={polsekState.create.form.namaTempatMaps}
onChange={(val) => {
polsekState.create.form.namaTempatMaps = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Tempat Maps Polsek Terdekat</Text>}
placeholder='Masukkan nama tempat maps Polsek Terdekat'
/>
<TextInput
value={polsekState.create.form.alamatMaps}
onChange={(val) => {
polsekState.create.form.alamatMaps = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Alamat Maps Polsek Terdekat</Text>}
placeholder='Masukkan alamat maps Polsek Terdekat'
/>
<TextInput
value={polsekState.create.form.linkPetunjukArah}
onChange={(val) => {
polsekState.create.form.linkPetunjukArah = val.target.value
}}
label={<Text fw={"bold"} fz={"sm"}>Link Petunjuk Arah Polsek Terdekat</Text>}
placeholder='Masukkan link petunjuk arah Polsek Terdekat'
/>
{/* Tambah Dropdown Select */}
<Select
label="Layanan Polsek"
placeholder="Pilih layanan polsek"
data={layananOptions}
value={polsekState.create.form.layananPolsekId}
onChange={(val) => {
polsekState.create.form.layananPolsekId = val || "";
}}
/>
<Button variant="light" size="xs" onClick={() => setModalOpen(true)}>
+ Tambah Layanan Baru
</Button>
{/* Tambah field lainnya di sini... */}
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePolsekTerdekat;

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 DetailPolsekTerdekat() {
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 Polsek Terdekat</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Polsek Terdekat</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nomor Polsek Terdekat</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/keamanan/polsek-terdekat/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 DetailPolsekTerdekat;

View File

@@ -1,44 +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 { KeamananEditor } from '../../_com/keamananEditor';
function EditPolsekTerdekat() {
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 Polsek Terdekat</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Polsek Terdekat</Text>}
placeholder='Masukkan nama Polsek Terdekat'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Polsek Terdekat</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPolsekTerdekat;

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, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import polsekTerdekat from '../../_state/keamanan/polsek-terdekat';
function PolsekTerdekat() {
return (
@@ -20,7 +23,20 @@ function PolsekTerdekat() {
}
function ListPolsekTerdekat() {
const polsekState = useProxy(polsekTerdekat)
const router = useRouter();
useShallowEffect(() => {
polsekState.findMany.load()
}, [])
if (!polsekState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -32,22 +48,24 @@ function ListPolsekTerdekat() {
<TableThead>
<TableTr>
<TableTh>Nama Polsek Terdekat</TableTh>
<TableTh>Nomor Polsek Terdekat</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Jarak Polsek</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Polsek Terdekat 1</TableTd>
<TableTd>0896232831883</TableTd>
<TableTd>Polsek Terdekat 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/keamanan/polsek-terdekat/detail')}>
{polsekState.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.jarakKeDesa}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/polsek-terdekat/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>

View File

@@ -0,0 +1,179 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import {
Box,
Button,
Center,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title
} from "@mantine/core";
import { IconArrowBack, IconImageInPicture, 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";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
import { Dropzone } from "@mantine/dropzone";
import tipsKeamananState from "../../../../_state/keamanan/tips-keamanan";
function EditTipsKeamanan() {
const keamananState = useProxy(tipsKeamananState);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
judul: keamananState.update.form.judul || '',
deskripsi: keamananState.update.form.deskripsi || '',
imageId: keamananState.update.form.imageId || ''
});
// Load berita by id saat pertama kali
useEffect(() => {
const loadBerita = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await keamananState.update.load(id); // akses langsung, bukan dari proxy
if (data) {
setFormData({
judul: data.judul || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading keamananLingkungan:", error);
toast.error("Gagal memuat data keamananLingkungan");
}
};
loadBerita();
}, [params?.id]); // ✅ hapus beritaState dari dependency
const handleSubmit = async () => {
try {
// Update global state with form data
keamananState.update.form = {
...keamananState.update.form,
judul: formData.judul,
deskripsi: formData.deskripsi,
imageId: formData.imageId // Keep existing imageId if not changed
};
// Jika ada file baru, upload
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");
}
// Update imageId in global state
keamananState.update.form.imageId = uploaded.id;
}
await keamananState.update.update();
toast.success("Tips Keamanan berhasil diperbarui!");
router.push("/admin/keamanan/tips-keamanan");
} catch (error) {
console.error("Error updating keamananLingkungan:", error);
toast.error("Terjadi kesalahan saat memperbarui keamananLingkungan");
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Tips Keamanan</Title>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<TextInput
value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama Tips Keamanan</Text>}
placeholder="masukkan nama tips keamanan"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
keamananState.update.form.deskripsi = htmlContent;
}}
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditTipsKeamanan;

View File

@@ -0,0 +1,111 @@
'use client'
import { useProxy } from 'valtio/utils';
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 colors from '@/con/colors';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import tipsKeamananState from '../../../_state/keamanan/tips-keamanan';
function DetailTipsKeamanan() {
const stateKeamanan = useProxy(tipsKeamananState)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
stateKeamanan.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
stateKeamanan.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/keamanan/tips-keamanan")
}
}
if (!stateKeamanan.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
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']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Tips Keamanan</Text>
{stateKeamanan.findUnique.data ? (
<Paper key={stateKeamanan.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Tips Keamanan</Text>
<Text fz={"lg"}>{stateKeamanan.findUnique.data?.judul}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateKeamanan.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateKeamanan.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateKeamanan.findUnique.data) {
setSelectedId(stateKeamanan.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateKeamanan.delete.loading || !stateKeamanan.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateKeamanan.findUnique.data) {
router.push(`/admin/keamanan/tips-keamanan/${stateKeamanan.findUnique.data.id}/edit`);
}
}}
disabled={!stateKeamanan.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus tips keamanan ini?'
/>
</Box>
);
}
export default DetailTipsKeamanan;

View File

@@ -1,44 +1,148 @@
'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 ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../_com/keamananEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import tipsKeamananState from '../../../_state/keamanan/tips-keamanan';
function CreateTipsKeamanan() {
function CreateKeamananLingkungan() {
const stateKeamanan = useProxy(tipsKeamananState)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter();
const resetForm = () => {
stateKeamanan.create.form = {
judul: "",
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 mengupload file");
}
stateKeamanan.create.form.imageId = uploaded.id;
await stateKeamanan.create.create();
resetForm();
router.push("/admin/keamanan/tips-keamanan")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Tips Keamanan</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Tips Keamanan</Text>}
placeholder='Masukkan nama tips keamanan'
value={stateKeamanan.create.form.judul}
onChange={(val) => {
stateKeamanan.create.form.judul = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Tips Keamanan</Text>}
placeholder='Masukkan nama Tips Keamanan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
<KeamananEditor
showSubmit={false}
<CreateEditor
value={stateKeamanan.create.form.deskripsi}
onChange={(val) => {
stateKeamanan.create.form.deskripsi = val;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateTipsKeamanan;
export default CreateKeamananLingkungan;

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 DetailTipsKeamanan() {
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 Tips Keamanan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Tips Keamanan</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nomor Tips Keamanan</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/keamanan/tips-keamanan/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 DetailTipsKeamanan;

View File

@@ -1,44 +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 { KeamananEditor } from '../../_com/keamananEditor';
function EditTipsKeamanan() {
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 Tips Keamanan</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Tips Keamanan</Text>}
placeholder='Masukkan nama Tips Keamanan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Tips Keamanan</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditTipsKeamanan;

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, Stack, 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 tipsKeamananState from '../../_state/keamanan/tips-keamanan';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
function TipsKeamanan() {
return (
@@ -20,7 +23,20 @@ function TipsKeamanan() {
}
function ListTipsKeamanan() {
const stateKeamanan = useProxy(tipsKeamananState)
const router = useRouter();
useShallowEffect(() => {
stateKeamanan.findMany.load()
}, [])
if (!stateKeamanan.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -32,22 +48,24 @@ function ListTipsKeamanan() {
<TableThead>
<TableTr>
<TableTh>Nama Tips Keamanan</TableTh>
<TableTh>Nomor Tips Keamanan</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Tips Keamanan 1</TableTd>
<TableTd>0896232831883</TableTd>
<TableTd>Tips Keamanan 1</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/keamanan/tips-keamanan/detail')}>
<IconDeviceImac size={20} />
{stateKeamanan.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.judul}</TableTd>
<TableTd>
<Text fz={"xs"} dangerouslySetInnerHTML={{__html: item.deskripsi}} />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/tips-keamanan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>

View File

@@ -0,0 +1,374 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors';
import { Box, Button, 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';
interface ArtikelKesehatanFormBase {
title: string;
content: string;
introduction: {
content: string;
};
symptom: {
title: string;
content: string;
};
prevention: {
title: string;
content: string;
};
firstAid: {
title: string;
content: string;
};
mythVsFact: {
title: string;
mitos: string;
fakta: string;
};
doctorSign: {
content: string;
};
}
function EditArtikelKesehatan() {
const stateArtikelKesehatan = useProxy(artikelKesehatanState);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState<ArtikelKesehatanFormBase>({
title: stateArtikelKesehatan.edit.form.title || '',
content: stateArtikelKesehatan.edit.form.content || '',
introduction: {
content: stateArtikelKesehatan.edit.form.introduction?.content || '',
},
symptom: {
title: stateArtikelKesehatan.edit.form.symptom?.title || '',
content: stateArtikelKesehatan.edit.form.symptom?.content || '',
},
prevention: {
title: stateArtikelKesehatan.edit.form.prevention?.title || '',
content: stateArtikelKesehatan.edit.form.prevention?.content || '',
},
firstAid: {
title: stateArtikelKesehatan.edit.form.firstAid?.title || '',
content: stateArtikelKesehatan.edit.form.firstAid?.content || '',
},
mythVsFact: {
title: stateArtikelKesehatan.edit.form.mythVsFact?.title || '',
mitos: stateArtikelKesehatan.edit.form.mythVsFact?.mitos || '',
fakta: stateArtikelKesehatan.edit.form.mythVsFact?.fakta || '',
},
doctorSign: {
content: stateArtikelKesehatan.edit.form.doctorSign?.content || '',
},
});
useEffect(() => {
const loadArtikelKesehatan = async () => {
const id = params?.id as string;
if (!id) return;
try {
await stateArtikelKesehatan.edit.load(id);
const { form } = stateArtikelKesehatan.edit;
if (form) {
setFormData({
title: form.title,
content: form.content,
introduction: {
content: form.introduction?.content || '',
},
symptom: {
title: form.symptom?.title || '',
content: form.symptom?.content || '',
},
prevention: {
title: form.prevention?.title || '',
content: form.prevention?.content || '',
},
firstAid: {
title: form.firstAid?.title || '',
content: form.firstAid?.content || '',
},
mythVsFact: {
title: form.mythVsFact?.title || '',
mitos: form.mythVsFact?.mitos || '',
fakta: form.mythVsFact?.fakta || '',
},
doctorSign: {
content: form.doctorSign?.content || '',
},
});
}
} catch (error) {
console.error("Error loading artikel kesehatan:", error);
toast.error("Gagal memuat data artikel kesehatan");
}
};
loadArtikelKesehatan();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateArtikelKesehatan.edit.form = {
...stateArtikelKesehatan.edit.form,
title: formData.title,
content: formData.content,
introduction: {
content: formData.introduction.content,
},
symptom: {
title: formData.symptom.title,
content: formData.symptom.content,
},
prevention: {
title: formData.prevention.title,
content: formData.prevention.content,
},
firstAid: {
title: formData.firstAid.title,
content: formData.firstAid.content,
},
mythVsFact: {
title: formData.mythVsFact.title,
mitos: formData.mythVsFact.mitos,
fakta: formData.mythVsFact.fakta,
},
doctorSign: {
content: formData.doctorSign.content,
},
};
const success = await stateArtikelKesehatan.edit.submit();
if (success) {
toast.success("Artikel kesehatan berhasil diperbarui!");
router.push("/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan");
}
} catch (error) {
console.error("Error updating artikel kesehatan:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data artikel kesehatan");
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant="subtle" color="blue">
<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}>Edit Artikel Kesehatan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
value={formData.title}
onChange={(e) => {
setFormData(prev => ({
...prev,
title: e.target.value
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
value={formData.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
content: e.target.value
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Pendahuluan</Text>}
placeholder="masukkan pendahuluan"
value={formData.introduction.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
introduction: {
...prev.introduction,
content: e.target.value
}
}));
}}
/>
<Box>
<Text fz="md" fw="bold">Gejala</Text>
<Stack gap="xs">
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul gejala penyakit"
value={formData.symptom.title}
onChange={(e) => {
setFormData(prev => ({
...prev,
symptom: {
...prev.symptom,
title: e.target.value
}
}));
}}
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi Gejala</Text>
<EditEditor
value={formData.symptom.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
symptom: {
...prev.symptom,
content: e
}
}));
}}
/>
</Box>
</Stack>
</Box>
<Box>
<Text fz="md" fw="bold">Pencegahan</Text>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
value={formData.prevention.title}
onChange={(e) => {
setFormData(prev => ({
...prev,
prevention: {
...prev.prevention,
title: e.target.value
}
}));
}}
/>
<EditEditor
value={formData.prevention.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
prevention: {
...prev.prevention,
content: e
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Pertolongan Pertama</Text>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
value={formData.firstAid.title}
onChange={(e) => {
setFormData(prev => ({
...prev,
firstAid: {
...prev.firstAid,
title: e.target.value
}
}));
}}
/>
<EditEditor
value={formData.firstAid.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
firstAid: {
...prev.firstAid,
content: e
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Mitos dan Fakta</Text>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
value={formData.mythVsFact.title}
onChange={(e) => {
setFormData(prev => ({
...prev,
mythVsFact: {
...prev.mythVsFact,
title: e.target.value
}
}));
}}
/>
<Box>
<Text>
Mitos
</Text>
<EditEditor
value={formData.mythVsFact.mitos}
onChange={(e) => {
setFormData(prev => ({
...prev,
mythVsFact: {
...prev.mythVsFact,
mitos: e
}
}));
}}
/>
</Box>
<Box>
<Text>
Fakta
</Text>
<EditEditor
value={formData.mythVsFact.fakta}
onChange={(e) => {
setFormData(prev => ({
...prev,
mythVsFact: {
...prev.mythVsFact,
fakta: e
}
}));
}}
/>
</Box>
</Box>
<Box>
<Text fz="md" fw="bold">Kapan Harus Ke Dokter</Text>
<EditEditor
value={formData.doctorSign.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
doctorSign: {
...prev.doctorSign,
content: e
}
}));
}}
/>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditArtikelKesehatan;

View File

@@ -0,0 +1,143 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailArtikelKesehatan() {
const params = useParams()
const router = useRouter();
const stateArtikelKesehatan = useProxy(artikelKesehatanState)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
stateArtikelKesehatan.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
stateArtikelKesehatan.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan")
}
}
if (!stateArtikelKesehatan.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 Artikel Kesehatan</Text>
{stateArtikelKesehatan.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.title}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.content }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Pendahuluan</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.introduction.content }} />
</Box>
<Box>
<Stack gap={"xs"}>
<Text fz={"lg"} fw={"bold"}>Gejala</Text>
<Text fz={"md"} fw={"bold"}>Judul Gejala</Text>
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.symptom.title}</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Gejala</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.symptom.content }} />
</Stack>
</Box>
<Box>
<Stack gap={"xs"}>
<Text fz={"lg"} fw={"bold"}>Pencegahan</Text>
<Text fz={"md"} fw={"bold"}>Judul Pencegahan</Text>
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.prevention.title}</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Pencegahan</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.prevention.content }} />
</Stack>
</Box>
<Box>
<Stack gap={"xs"}>
<Text fz={"lg"} fw={"bold"}>Pertolongan Pertama</Text>
<Text fz={"md"} fw={"bold"}>Judul Pertolongan Pertama</Text>
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.firstaid.title}</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Pertolongan Pertama</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.firstaid.content }} />
</Stack>
</Box>
<Box>
<Stack gap={"xs"}>
<Text fz={"lg"} fw={"bold"}>Mitos dan Fakta</Text>
<Text fz={"md"} fw={"bold"}>Judul Mitos dan Fakta</Text>
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.mythvsfact.title}</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Mitos</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.mythvsfact.mitos }} />
<Text fz={"md"} fw={"bold"}>Deskripsi Fakta</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.mythvsfact.fakta }} />
</Stack>
</Box>
<Box>
<Stack gap={"xs"}>
<Text fz={"lg"} fw={"bold"}>Kapan Harus ke Dokter</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Kapan Harus ke Dokter</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.doctorsign.content }} />
</Stack>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (stateArtikelKesehatan.findUnique.data) {
setSelectedId(stateArtikelKesehatan.findUnique.data.id)
setModalHapus(true)
}
}}>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${stateArtikelKesehatan.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 artikel kesehatan ini?"
/>
</Box>
);
}
export default DetailArtikelKesehatan;

View File

@@ -0,0 +1,203 @@
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateArtikelKesehatan() {
const stateArtikelKesehatan = useProxy(artikelKesehatanState)
const router = useRouter();
const resetForm = () => {
stateArtikelKesehatan.create.form = {
title: "",
content: "",
introduction: {
content: "",
},
symptom: {
title: "",
content: "",
},
prevention: {
title: "",
content: "",
},
firstAid: {
title: "",
content: "",
},
mythVsFact: {
title: "",
mitos: "",
fakta: "",
},
doctorSign: {
content: ""
}
};
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await stateArtikelKesehatan.create.submit();
toast.success("Data berhasil disimpan");
resetForm();
// After successful submission, redirect to the list page
router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan');
}
return (
<Box component="form" onSubmit={handleSubmit}>
<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 Artikel Kesehatan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
value={stateArtikelKesehatan.create.form.title}
onChange={(e) => {
stateArtikelKesehatan.create.form.title = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
value={stateArtikelKesehatan.create.form.content}
onChange={(e) => {
stateArtikelKesehatan.create.form.content = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Pendahuluan</Text>}
placeholder="masukkan pendahuluan"
value={stateArtikelKesehatan.create.form.introduction.content}
onChange={(e) => {
stateArtikelKesehatan.create.form.introduction.content = e.target.value;
}}
/>
<Box>
<Text fz="md" fw="bold">Gejala</Text>
<Stack gap="xs">
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul gejala penyakit"
value={stateArtikelKesehatan.create.form.symptom.title}
onChange={(e) => {
stateArtikelKesehatan.create.form.symptom.title = e.target.value;
}}
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi Gejala</Text>
<CreateEditor
value={stateArtikelKesehatan.create.form.symptom.content}
onChange={(e) => {
stateArtikelKesehatan.create.form.symptom.content = e;
}}
/>
</Box>
</Stack>
</Box>
<Box>
<Text fz="md" fw="bold">Pencegahan</Text>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
value={stateArtikelKesehatan.create.form.prevention.title}
onChange={(e) => {
stateArtikelKesehatan.create.form.prevention.title = e.target.value;
}}
/>
<CreateEditor
value={stateArtikelKesehatan.create.form.prevention.content}
onChange={(e) => {
stateArtikelKesehatan.create.form.prevention.content = e;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Pertolongan Pertama</Text>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
value={stateArtikelKesehatan.create.form.firstAid.title}
onChange={(e) => {
stateArtikelKesehatan.create.form.firstAid.title = e.target.value;
}}
/>
<CreateEditor
value={stateArtikelKesehatan.create.form.firstAid.content}
onChange={(e) => {
stateArtikelKesehatan.create.form.firstAid.content = e;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Mitos dan Fakta</Text>
<TextInput
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
value={stateArtikelKesehatan.create.form.mythVsFact.title}
onChange={(e) => {
stateArtikelKesehatan.create.form.mythVsFact.title = e.target.value;
}}
/>
<Box>
<Text>
Mitos
</Text>
<CreateEditor
value={stateArtikelKesehatan.create.form.mythVsFact.mitos}
onChange={(e) => {
stateArtikelKesehatan.create.form.mythVsFact.mitos = e;
}}
/>
</Box>
<Box>
<Text>
Fakta
</Text>
<CreateEditor
value={stateArtikelKesehatan.create.form.mythVsFact.fakta}
onChange={(e) => {
stateArtikelKesehatan.create.form.mythVsFact.fakta = e;
}}
/>
</Box>
</Box>
<Box>
<Text fz="md" fw="bold">Kapan Harus Ke Dokter</Text>
<CreateEditor
value={stateArtikelKesehatan.create.form.doctorSign.content}
onChange={(e) => {
stateArtikelKesehatan.create.form.doctorSign.content = e;
}}
/>
</Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreateArtikelKesehatan;

View File

@@ -1,25 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Paper, Text } from '@mantine/core';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import stateArtikelKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import { useProxy } from 'valtio/utils';
function DoctorSignUI() {
const doctorSign = useProxy(stateArtikelKesehatan.doctorSign)
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Text fw={"bold"}>Kapan Harus ke Dokter</Text>
<KesehatanEditor
showSubmit={false}
onChange={(val) => {
doctorSign.create.form.content = val
}}
/>
</Paper>
</Box>
);
}
export default DoctorSignUI;

View File

@@ -1,33 +0,0 @@
'use client'
import stateArtikelKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors';
import { Box, Paper, Stack, Text, TextInput } from '@mantine/core';
import { useProxy } from 'valtio/utils';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
function FirstAidUI() {
const firstAidState = useProxy(stateArtikelKesehatan.firstAid)
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<TextInput
label={<Text fw={"bold"}>Judul Pertolongan Pertama</Text>}
placeholder="Masukkan judul"
onChange={(val) => {
firstAidState.create.form.title = val.target.value
}}
/>
<KesehatanEditor
showSubmit={false}
onChange={(val) => {
firstAidState.create.form.content = val
}}
/>
</Stack>
</Paper>
</Box>
);
}
export default FirstAidUI;

View File

@@ -1,28 +0,0 @@
'use client'
import { Box, Paper, Stack, Text } from '@mantine/core';
import React from 'react';
import { useProxy } from 'valtio/utils';
import colors from '@/con/colors';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
import stateArtikelKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
function IntoductionUI() {
const introduction = useProxy(stateArtikelKesehatan.introduction)
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Text fw={"bold"}>Pendahuluan</Text>
<KesehatanEditor
showSubmit={false}
onChange={(val) => {
introduction.create.form.content = val;
}}
/>
</Stack>
</Paper>
</Box>
);
}
export default IntoductionUI;

View File

@@ -1,40 +0,0 @@
'use client'
import stateArtikelKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors';
import { Box, Paper, Stack, Text, TextInput } from '@mantine/core';
import { useProxy } from 'valtio/utils';
function MythFactUI() {
const mythFact = useProxy(stateArtikelKesehatan.mythFact)
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<TextInput
label={<Text fw={"bold"}>Judul Pertolongan Pertama Penyakit</Text>}
placeholder="Masukkan judul"
onChange={(val) => {
mythFact.create.form.title = val.target.value
}}
/>
<TextInput
label={<Text fw={"bold"}>Mitos</Text>}
placeholder="Masukkan mitos"
onChange={(val) => {
mythFact.create.form.mitos = val.target.value
}}
/>
<TextInput
label={<Text fw={"bold"}>Fakta</Text>}
placeholder="Masukkan fakta"
onChange={(val) => {
mythFact.create.form.fakta = val.target.value
}}
/>
</Stack>
</Paper>
</Box>
);
}
export default MythFactUI;

View File

@@ -1,172 +1,79 @@
'use client'
import { Box, Button, Center, Group, Paper, SimpleGrid, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import IntoductionUI from './introduction/page';
import SymptomUI from './symptom/page';
import PreventionUI from './prevention/page';
import MythFactUI from './mythVsfact/page';
import DoctorSignUI from './doctor_sign/page';
import { useProxy } from 'valtio/utils';
import stateArtikelKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import FirstAidUI from './first_aid/page';
import { useShallowEffect } from '@mantine/hooks';
import colors from '@/con/colors';
import { Box, Button, 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 { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import artikelKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
function ArtikelKesehatan() {
const state = useProxy(stateArtikelKesehatan)
const submitAllForms = () => {
if (state.introduction.create.form.content) {
state.introduction.create.create()
}
if (state.symptom.create.form.title && state.symptom.create.form.content) {
state.symptom.create.create()
}
if (state.prevention.create.form.title && state.prevention.create.form.content) {
state.prevention.create.create()
}
if (state.firstAid.create.form.title && state.firstAid.create.form.content) {
state.firstAid.create.create()
}
if (state.mythFact.create.form.title && state.mythFact.create.form.mitos && state.mythFact.create.form.fakta) {
state.mythFact.create.create()
}
if (state.doctorSign.create.form.content) {
state.doctorSign.create.create()
}
}
return (
<Stack py={10}>
<SimpleGrid cols={{
base: 1, md: 2
}}>
<Box >
<Stack gap={"xs"}>
<Title order={4}>Artikel Kesehatan</Title>
<IntoductionUI />
<SymptomUI />
<PreventionUI />
<FirstAidUI />
<MythFactUI />
<DoctorSignUI />
<Group>
<Button bg={colors['blue-button']} mt={10} onClick={submitAllForms}>Submit</Button>
</Group>
</Stack>
</Box>
<Box>
<Stack gap={"xs"}>
<Title order={4}>Data Artikel Kesehatan</Title>
<AllList />
</Stack>
</Box>
</SimpleGrid>
</Stack>
<Box>
<HeaderSearch
title='Artikel Kesehatan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListArtikelKesehatan/>
</Box>
);
}
function AllList() {
const listState = useProxy(stateArtikelKesehatan)
function ListArtikelKesehatan() {
const stateArtikelKesehatan = useProxy(artikelKesehatanState)
const router = useRouter();
useShallowEffect(() => {
listState.introduction.findMany.load();
listState.symptom.findMany.load();
listState.prevention.findMany.load();
listState.firstAid.findMany.load();
listState.mythFact.findMany.load();
listState.doctorSign.findMany.load();
stateArtikelKesehatan.findMany.load()
}, [])
if (!listState.introduction.findMany.data
|| !listState.symptom.findMany.data
|| !listState.prevention.findMany.data
|| !listState.firstAid.findMany.data
|| !listState.mythFact.findMany.data
|| !listState.doctorSign.findMany.data
) return <Stack>
{Array.from({ length: 10 }).map((v, k) => <Skeleton key={k} h={40} />)}
</Stack>
return <Stack gap={"xs"}>
{/* Introduction */}
if (!stateArtikelKesehatan.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500}/>
</Box>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Title order={4}>Pendahuluan</Title>
{listState.introduction.findMany.data?.map((item) => (
<Box key={item.id}>
<Text dangerouslySetInnerHTML={{ __html: item.content }}></Text>
</Box>
))}
</Paper>
{/* Symptom */}
<Paper bg={colors['white-1']} p={'md'}>
<Title order={4}>Gejala Penyakit</Title>
{listState.symptom.findMany.data?.map((item) => (
<Box key={item.id}>
<Title order={4}>{item.title}</Title>
<Text dangerouslySetInnerHTML={{ __html: item.content }}></Text>
</Box>
))}
</Paper>
{/* Prevention */}
<Paper bg={colors['white-1']} p={'md'}>
<Title order={4}>Pencegahan Penyakit</Title>
{listState.prevention.findMany.data?.map((item) => (
<Box key={item.id}>
<Title order={4}>{item.title}</Title>
<Text dangerouslySetInnerHTML={{ __html: item.content }}></Text>
</Box>
))}
</Paper>
{/* First Aid */}
<Paper bg={colors['white-1']} p={'md'}>
<Title order={4}>Pertolongan Pertama</Title>
{listState.firstAid.findMany.data?.map((item) => (
<Box key={item.id}>
<Title order={4}>{item.title}</Title>
<Text dangerouslySetInnerHTML={{ __html: item.content }}></Text>
</Box>
))}
</Paper>
{/* Myth Fact */}
<Paper bg={colors['white-1']} p={'md'}>
<Title order={4}>Mitos vs Fakta</Title>
{listState.mythFact.findMany.data?.map((item) => (
<Box key={item.id}>
<Title order={4}>{item.title}</Title>
<Table
striped
highlightOnHover
withTableBorder
withColumnBorders
bg={colors['white-1']}
>
<TableThead >
<TableTr >
<TableTh >
<Center>Mitos</Center>
</TableTh>
<TableTh >
<Center>Fakta</Center>
</TableTh>
<Stack>
<JudulList
title='List Artikel Kesehatan'
href='/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Content</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody >
<TableTr>
<TableTd ta="center">{item.mitos}</TableTd>
<TableTd ta="center">{item.fakta}</TableTd>
</TableTr>
<TableTbody>
{stateArtikelKesehatan.findMany.data?.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.title}</TableTd>
<TableTd>{item.content}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
))}
</Stack>
</Paper>
{/* Doctor Sign */}
<Paper bg={colors['white-1']} p={'md'}>
<Title order={4}>Kapan Harus Ke Dokter?</Title>
{listState.doctorSign.findMany.data?.map((item) => (
<Box key={item.id}>
<Text dangerouslySetInnerHTML={{ __html: item.content }} />
</Box>
))}
</Paper>
</Stack>
</Box>
)
}
export default ArtikelKesehatan;
export default ArtikelKesehatan;

View File

@@ -1,34 +0,0 @@
'use client'
import stateArtikelKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors';
import { Box, Paper, Stack, Text, TextInput } from '@mantine/core';
import { useProxy } from 'valtio/utils';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
function PreventionUI() {
const preventionState = useProxy(stateArtikelKesehatan.prevention)
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<TextInput
mb={10}
label={<Text fw={"bold"}>Judul Pencegahan Penyakit</Text>}
placeholder="Masukkan judul"
onChange={(val) => {
preventionState.create.form.title = val.target.value
}}
/>
<KesehatanEditor
showSubmit={false}
onChange={(val) => {
preventionState.create.form.content = val
}}
/>
</Stack>
</Paper>
</Box>
);
}
export default PreventionUI;

View File

@@ -1,34 +0,0 @@
'use client'
import stateArtikelKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import colors from '@/con/colors';
import { Box, Paper, Stack, Text, TextInput } from '@mantine/core';
import { useProxy } from 'valtio/utils';
import { KesehatanEditor } from '../../_com/kesehatanEditor';
function SymptomUI() {
const symptomState = useProxy(stateArtikelKesehatan.symptom)
return (
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<TextInput
mb={10}
label={<Text fw={"bold"}>Judul Gejala Penyakit</Text>}
placeholder='masukkan judul'
onChange={(val) => {
symptomState.create.form.title = val.target.value
}}
/>
<KesehatanEditor
showSubmit={false}
onChange={(val) => {
symptomState.create.form.content = val
}}
/>
</Stack>
</Paper>
</Box>
);
}
export default SymptomUI;

View File

@@ -11,15 +11,15 @@ import { useProxy } from 'valtio/utils';
function EditGrafikHasilKepuasan() {
const router = useRouter()
const params = useParams() as { uuid: string }
const params = useParams() as { id: string }
const stateGrafikKepuasan = useProxy(grafikkepuasan)
const uuid = params.uuid
const id = params.id
// Load data saat komponen mount
useEffect(() => {
if (uuid) {
stateGrafikKepuasan.findUnique.load(uuid).then(() => {
if (id) {
stateGrafikKepuasan.findUnique.load(id).then(() => {
const data = stateGrafikKepuasan.findUnique.data
if (data) {
stateGrafikKepuasan.update.form = {
@@ -29,11 +29,11 @@ function EditGrafikHasilKepuasan() {
}
})
}
}, [uuid])
}, [id])
const handleSubmit = async () => {
// Set the ID before submitting
stateGrafikKepuasan.update.uuid = uuid;
stateGrafikKepuasan.update.id = id;
await stateGrafikKepuasan.update.submit();
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan')
}

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