diff --git a/prisma/data/ekonomi/struktur-organisasi/hubungan-organisasi.json b/prisma/data/ekonomi/struktur-organisasi/hubungan-organisasi.json new file mode 100644 index 00000000..1bef7d69 --- /dev/null +++ b/prisma/data/ekonomi/struktur-organisasi/hubungan-organisasi.json @@ -0,0 +1,8 @@ +[ + { + "id": "650e8400-e29b-41d4-a716-446655440001", + "atasanId": "550e8400-e29b-41d4-a716-446655440001", + "bawahanId": "550e8400-e29b-41d4-a716-446655440002", + "tipe": "langsung_melapor" + } +] diff --git a/prisma/data/ekonomi/struktur-organisasi/pegawai.json b/prisma/data/ekonomi/struktur-organisasi/pegawai.json new file mode 100644 index 00000000..80578dad --- /dev/null +++ b/prisma/data/ekonomi/struktur-organisasi/pegawai.json @@ -0,0 +1,24 @@ +[ + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "namaLengkap": "Budi Santoso", + "gelarAkademik": "S.IP", + "tanggalMasuk": "2020-01-01T00:00:00.000Z", + "email": "budi@desa.id", + "telepon": "081234567891", + "alamat": "Jl. Raya Desa No. 1", + "posisiId": "kepala_desa", + "aktif": true + }, + { + "id": "550e8400-e29b-41d4-a716-446655440002", + "namaLengkap": "Ani Lestari", + "gelarAkademik": "S.Pd", + "tanggalMasuk": "2020-02-01T00:00:00.000Z", + "email": "ani@desa.id", + "telepon": "081234567892", + "alamat": "Jl. Raya Desa No. 2", + "posisiId": "sekretaris_desa", + "aktif": true + } + ] \ No newline at end of file diff --git a/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi.json b/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi.json new file mode 100644 index 00000000..7596e168 --- /dev/null +++ b/prisma/data/ekonomi/struktur-organisasi/posisi-organisasi.json @@ -0,0 +1,27 @@ +[ + { + "id": "kepala_desa", + "nama": "Kepala Desa", + "deskripsi": "Kepala Desa", + "hierarki": 1 + }, + { + "id": "sekretaris_desa", + "nama": "Sekretaris Desa", + "deskripsi": "Sekretaris Desa", + "hierarki": 2 + }, + { + "id": "bendahara_desa", + "nama": "Bendahara Desa", + "deskripsi": "Bendahara Desa", + "hierarki": 3 + }, + { + "id": "staff_umum", + "nama": "Staff Umum", + "deskripsi": "Staff Umum", + "hierarki": 4 + } + ] + \ No newline at end of file diff --git a/prisma/migrations/20250704091225_4_jul_2025/migration.sql b/prisma/migrations/20250704091225_4_jul_2025/migration.sql new file mode 100644 index 00000000..b8f75b46 --- /dev/null +++ b/prisma/migrations/20250704091225_4_jul_2025/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `kategoriProdukId` to the `PasarDesa` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "PasarDesa" ADD COLUMN "kategoriProdukId" TEXT NOT NULL; + +-- AddForeignKey +ALTER TABLE "PasarDesa" ADD CONSTRAINT "PasarDesa_kategoriProdukId_fkey" FOREIGN KEY ("kategoriProdukId") REFERENCES "KategoriProduk"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2fa3ef58..c81f6abb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -88,6 +88,8 @@ model FileStorage { KontakDaruratKeamanan KontakDaruratKeamanan[] KontakItem KontakItem[] + + Pegawai Pegawai[] } //========================================= MENU PPID ========================================= // @@ -1050,20 +1052,20 @@ model MenuTipsKeamanan { // ========================================= 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]) + 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[] + KategoriToPasar KategoriToPasar[] } model KategoriProduk { @@ -1074,7 +1076,7 @@ model KategoriProduk { deletedAt DateTime @default(now()) isActive Boolean @default(true) KategoriToPasar KategoriToPasar[] - PasarDesa PasarDesa[] + PasarDesa PasarDesa[] } model KategoriToPasar { @@ -1091,21 +1093,93 @@ model KategoriToPasar { // ========================================= 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 + id String @id @default(uuid()) + posisi String + namaPerusahaan String + lokasi String + tipePekerjaan String + gaji String + deskripsi String + kualifikasi String + tanggalPosting DateTime @default(now()) + isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) } +// ========================================= STRUKTUR ORGANISASI ========================================= // + +model PosisiOrganisasi { + id String @id @default(uuid()) @db.VarChar(50) + nama String @db.VarChar(100) + deskripsi String? @db.Text + hierarki Int + + pegawai Pegawai[] + strukturOrganisasi StrukturOrganisasi[] // Relasi balik + + @@map("posisi_organisasi") +} + +model Pegawai { + id String @id @default(uuid()) @db.Uuid + namaLengkap String @db.VarChar(255) + gelarAkademik String? @db.VarChar(100) + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? + tanggalMasuk DateTime? @db.Date + email String? @unique @db.VarChar(255) + telepon String? @db.VarChar(20) + alamat String? @db.Text + posisiId String @db.VarChar(50) + aktif Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + posisi PosisiOrganisasi @relation(fields: [posisiId], references: [id]) + + sebagaiAtasan HubunganOrganisasi[] @relation("AtasanToBawahan") + sebagaiBawahan HubunganOrganisasi[] @relation("BawahanToAtasan") + + strukturOrganisasi StrukturOrganisasi[] // Relasi balik + + @@map("pegawai") +} + +model HubunganOrganisasi { + id String @id @default(uuid()) @db.Uuid + atasanId String @db.Uuid + bawahanId String @db.Uuid + tipe String? @db.VarChar(50) + + atasan Pegawai @relation("AtasanToBawahan", fields: [atasanId], references: [id]) + bawahan Pegawai @relation("BawahanToAtasan", fields: [bawahanId], references: [id]) + + strukturOrganisasi StrukturOrganisasi[] // Relasi balik + + @@unique([atasanId, bawahanId]) + @@map("hubungan_organisasi") +} + +model StrukturOrganisasi { + id String @id @default(uuid()) + posisiOrganisasiId String @db.VarChar(50) + pegawaiId String @db.Uuid + hubunganOrganisasiId String @db.Uuid + + posisiOrganisasi PosisiOrganisasi @relation(fields: [posisiOrganisasiId], references: [id]) + pegawai Pegawai @relation(fields: [pegawaiId], references: [id]) + hubunganOrganisasi HubunganOrganisasi @relation(fields: [hubunganOrganisasiId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + isActive Boolean @default(true) + + @@map("struktur_organisasi") +} + // ========================================= PROGRAM KEMISKINAN ========================================= // model ProgramKemiskinan { id String @id @default(uuid()) @@ -1113,8 +1187,7 @@ model ProgramKemiskinan { 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 + statistikId String? @unique statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -1124,7 +1197,6 @@ 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 diff --git a/prisma/seed.ts b/prisma/seed.ts index 02fbdc5a..cda12440 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -18,6 +18,9 @@ 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"; +import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json"; +import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json"; +import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json"; (async () => { for (const l of layanan) { @@ -357,6 +360,75 @@ import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json"; }); } console.log("kategori produk success ..."); + + + for (const p of posisiOrganisasi) { + await prisma.posisiOrganisasi.upsert({ + where: { + id: p.id, + }, + update: { + nama: p.nama, + deskripsi: p.deskripsi, + hierarki: p.hierarki, + }, + create: { + id: p.id, + nama: p.nama, + deskripsi: p.deskripsi, + hierarki: p.hierarki, + }, + }); + } + console.log("posisi organisasi success ..."); + + for (const p of pegawai) { + await prisma.pegawai.upsert({ + where: { + id: p.id, + }, + update: { + namaLengkap: p.namaLengkap, + gelarAkademik: p.gelarAkademik, + tanggalMasuk: new Date(p.tanggalMasuk), + email: p.email, + telepon: p.telepon, + alamat: p.alamat, + posisiId: p.posisiId, + }, + create: { + id: p.id, + namaLengkap: p.namaLengkap, + gelarAkademik: p.gelarAkademik, + tanggalMasuk: new Date(p.tanggalMasuk), + email: p.email, + telepon: p.telepon, + alamat: p.alamat, + posisiId: p.posisiId, + }, + }); + } + console.log("pegawai success ..."); + + for (const p of hubunganOrganisasi) { + await prisma.hubunganOrganisasi.upsert({ + where: { + atasanId_bawahanId: { + atasanId: p.atasanId, + bawahanId: p.bawahanId, + }, + }, + update: { + tipe: p.tipe, + }, + create: { + atasanId: p.atasanId, + bawahanId: p.bawahanId, + tipe: p.tipe, + }, + }); + } + console.log("hubungan organisasi success ..."); })() .then(() => prisma.$disconnect()) .catch((e) => { diff --git a/src/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi.ts b/src/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi.ts new file mode 100644 index 00000000..3ae7f99f --- /dev/null +++ b/src/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi.ts @@ -0,0 +1,640 @@ +import { proxy } from "valtio"; +import { z } from "zod"; +import { toast } from "react-toastify"; +import ApiFetch from "@/lib/api-fetch"; +import { Prisma } from "@prisma/client"; + +const templatePosisiOrganisasi = z.object({ + nama: z.string().min(1, "Nama harus diisi"), + deskripsi: z.string().optional(), + hierarki: z.number().int().positive("Hierarki harus angka positif"), +}); + +const posisiOrganisasiDefaultForm = { + nama: "", + deskripsi: "", + hierarki: 0, +}; + +const posisiOrganisasi = proxy({ + create: { + form: { ...posisiOrganisasiDefaultForm }, + loading: false, + async submit() { + const cek = templatePosisiOrganisasi.safeParse(this.form); + if (!cek.success) { + const err = cek.error.issues.map((v) => v.message).join("\n"); + return toast.error(err); + } + + try { + this.loading = true; + const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["create"].post( + this.form + ); + if (res.status === 200) { + toast.success("Berhasil menambahkan posisi organisasi"); + posisiOrganisasi.findMany.load(); + this.reset(); + } else { + toast.error(res.data?.message || "Gagal menambahkan posisi"); + } + } catch (error) { + console.error("Create error:", error); + toast.error("Terjadi kesalahan saat menambahkan posisi"); + } finally { + this.loading = false; + } + }, + reset() { + this.form = { ...posisiOrganisasiDefaultForm }; + }, + }, + + edit: { + id: "", + form: { ...posisiOrganisasiDefaultForm }, + loading: false, + async load(id: string) { + if (!id) { + toast.warn("ID tidak valid"); + return null; + } + try { + const response = await fetch(`/api/ekonomi/struktur-organisasi/posisi-organisasi/${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, + hierarki: data.hierarki, + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading posisi organisasi:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; + } + }, + async update() { + const cek = templatePosisiOrganisasi.safeParse(this.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}`) + .join("\n")}] required`; + return toast.error(err); + } + + try { + this.loading = true; + const response = await fetch( + `/api/ekonomi/struktur-organisasi/posisi-organisasi/${this.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + nama: this.form.nama, + deskripsi: this.form.deskripsi, + hierarki: this.form.hierarki, + }), + } + ); + 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 posisi organisasi"); + await posisiOrganisasi.findMany.load(); // refresh list + return true; + } else { + throw new Error( + result.message || "Gagal mengupdate posisi organisasi" + ); + } + } catch (error) { + console.error("Error updating posisi organisasi:", error); + toast.error( + error instanceof Error + ? error.message + : "Gagal mengupdate posisi organisasi" + ); + return false; + } finally { + this.loading = false; + } + }, + reset() { + this.id = ""; + this.form = { ...posisiOrganisasiDefaultForm }; + }, + }, + + findMany: { + data: [] as Array<{ + id: string; + nama: string; + deskripsi: string | null; + hierarki: number; + }>, + async load() { + try { + const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get(); + if (res.status === 200) { + // The API now returns the id field, so we can use it directly + this.data = res.data?.data ?? []; + } + } catch (error) { + console.error("Find many error:", error); + this.data = []; + } + }, + }, + + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + posisiOrganisasi.delete.loading = true; + + const response = await fetch( + `/api/ekonomi/struktur-organisasi/posisi-organisasi/del/${id}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + + const result = await response.json(); + + if (response.ok && result?.success) { + toast.success(result.message || "Posisi organisasi berhasil dihapus"); + await posisiOrganisasi.findMany.load(); // refresh list + } else { + toast.error(result?.message || "Gagal menghapus posisi organisasi"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus posisi organisasi"); + } finally { + posisiOrganisasi.delete.loading = false; + } + }, + }, +}); + +const templatePegawai = z.object({ + namaLengkap: z.string().min(1, "Nama wajib diisi"), + gelarAkademik: z.string().optional(), + imageId: z.string().optional(), + tanggalMasuk: z.string().optional(), // ISO format + email: z.string().email("Email tidak valid").optional(), + telepon: z.string().optional(), + alamat: z.string().optional(), + posisiId: z.string().min(1, "Posisi wajib diisi"), + }); + + const pegawaiDefaultForm = { + namaLengkap: "", + gelarAkademik: "", + imageId: "", + tanggalMasuk: "", + email: "", + telepon: "", + alamat: "", + posisiId: "", + }; + + const pegawai = proxy({ + create: { + form: { ...pegawaiDefaultForm }, + loading: false, + async submit() { + const cek = templatePegawai.safeParse(pegawai.create.form); + if (!cek.success) { + const err = cek.error.issues.map(i => i.message).join("\n"); + toast.error(err); + return; + } + + try { + pegawai.create.loading = true; + const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["pegawai"]["create"].post( + pegawai.create.form + ); + if (res.status === 200) { + toast.success("Pegawai berhasil ditambahkan"); + await pegawai.findMany.load(); + } else { + toast.error(res.data?.message ?? "Gagal tambah pegawai"); + } + } catch (error) { + console.error("Gagal create:", error); + toast.error("Terjadi kesalahan saat menambahkan pegawai"); + } finally { + pegawai.create.loading = false; + } + }, + }, + + findMany: { + data: null as Prisma.PegawaiGetPayload<{ include: { posisi: true } }>[] | null, + async load() { + const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["pegawai"]["find-many"].get(); + if (res.status === 200) { + pegawai.findMany.data = res.data?.data ?? []; + } + }, + }, + + findUnique: { + data: null as Prisma.PegawaiGetPayload<{ include: { posisi: true } }> | null, + async load(id: string) { + const res = await fetch(`/api/ekonomi/strukturorganisasi/pegawai/${id}`); + if (res.ok) { + const json = await res.json(); + pegawai.findUnique.data = json.data ?? null; + } else { + pegawai.findUnique.data = null; + } + }, + }, + + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + try { + pegawai.delete.loading = true; + const res = await fetch(`/api/ekonomi/strukturorganisasi/pegawai/del/${id}`, { + method: "DELETE", + }); + const json = await res.json(); + if (res.ok) { + toast.success(json.message ?? "Berhasil hapus pegawai"); + await pegawai.findMany.load(); + } else { + toast.error(json.message ?? "Gagal hapus pegawai"); + } + } catch (error) { + console.error("Gagal delete:", error); + toast.error("Terjadi kesalahan saat menghapus"); + } finally { + pegawai.delete.loading = false; + } + }, + }, + + edit: { + id: "", + form: { ...pegawaiDefaultForm }, + loading: false, + async load(id: string) { + const res = await fetch(`/api/organisasi/pegawai/${id}`); + const json = await res.json(); + if (res.ok && json.success) { + pegawai.edit.id = json.data.id; + pegawai.edit.form = { + namaLengkap: json.data.namaLengkap ?? "", + gelarAkademik: json.data.gelarAkademik ?? "", + imageId: json.data.imageId ?? "", + tanggalMasuk: json.data.tanggalMasuk?.slice(0, 10) ?? "", + email: json.data.email ?? "", + telepon: json.data.telepon ?? "", + alamat: json.data.alamat ?? "", + posisiId: json.data.posisiId, + }; + } else { + toast.error("Gagal memuat data"); + } + }, + + async submit() { + const cek = templatePegawai.safeParse(pegawai.edit.form); + if (!cek.success) { + const err = cek.error.issues.map(i => i.message).join("\n"); + toast.error(err); + return; + } + + try { + pegawai.edit.loading = true; + const res = await fetch(`/api/ekonomi/strukturorganisasi/pegawai/${pegawai.edit.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(pegawai.edit.form), + }); + const json = await res.json(); + if (res.ok) { + toast.success(json.message ?? "Berhasil update pegawai"); + await pegawai.findMany.load(); + } else { + toast.error(json.message ?? "Gagal update pegawai"); + } + } catch (error) { + console.error("Gagal update:", error); + toast.error("Terjadi kesalahan saat update"); + } finally { + pegawai.edit.loading = false; + } + }, + + reset() { + pegawai.edit.id = ""; + pegawai.edit.form = { ...pegawaiDefaultForm }; + }, + }, + }); + + +// Schema Zod untuk form validasi +const templateHubunganOrganisasiForm = z.object({ + atasanId: z.string().min(1, "Atasan wajib dipilih"), + bawahanId: z.string().min(1, "Bawahan wajib dipilih"), + tipe: z.string().optional(), +}); + +// Default form state +const defaultHubunganOrganisasiForm = { + atasanId: "", + bawahanId: "", + tipe: "", +}; + +// ====================== STATE =========================== +const hubunganOrganisasi = proxy({ + create: { + form: { ...defaultHubunganOrganisasiForm }, + loading: false, + async create() { + const cek = templateHubunganOrganisasiForm.safeParse( + hubunganOrganisasi.create.form + ); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}: ${v.message}`) + .join("\n")}]`; + return toast.error(err); + } + + try { + hubunganOrganisasi.create.loading = true; + const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["hubungan-organisasi"]["create"].post(hubunganOrganisasi.create.form); + + if (res.status === 200 && res.data?.success) { + hubunganOrganisasi.findMany.load(); + return toast.success("Berhasil menambahkan hubungan organisasi"); + } else { + return toast.error(res.data?.message || "Gagal menambahkan data"); + } + } catch (error) { + console.error("Create Error:", error); + toast.error("Terjadi kesalahan saat menambahkan"); + } finally { + hubunganOrganisasi.create.loading = false; + } + }, + }, + + findMany: { + data: null as Array<{ + id: string; + atasanId: string; + bawahanId: string; + tipe?: string | null; + atasan: { + id: string; + namaLengkap: string; + gelarAkademik: string | null; + imageId: string | null; + tanggalMasuk: Date | null; + email: string | null; + telepon: string | null; + alamat: string | null; + posisiId: string; + aktif: boolean; + createdAt: Date; + updatedAt: Date; + }; + bawahan: { + id: string; + namaLengkap: string; + gelarAkademik: string | null; + imageId: string | null; + tanggalMasuk: Date | null; + email: string | null; + telepon: string | null; + alamat: string | null; + posisiId: string; + aktif: boolean; + createdAt: Date; + updatedAt: Date; + }; + }> | null, + + async load() { + try { + const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["hubungan-organisasi"]["find-many"].get(); + + if (res.status === 200 && res.data?.success) { + hubunganOrganisasi.findMany.data = res.data.data || []; + } else { + hubunganOrganisasi.findMany.data = []; + } + } catch (error) { + console.error("Fetch list error:", error); + toast.error("Gagal memuat data hubungan organisasi"); + hubunganOrganisasi.findMany.data = []; + } + }, + }, + + findUnique: { + data: null as { + id: string; + atasanId: string; + bawahanId: string; + tipe?: string | null; + atasan?: { + id: string; + namaLengkap: string; + gelarAkademik: string | null; + imageId: string; + tanggalMasuk: Date | null; + email: string | null; + telepon: string | null; + alamat: string | null; + posisiId: string; + aktif: boolean; + createdAt: Date; + updatedAt: Date; + }; + bawahan?: { + id: string; + namaLengkap: string; + gelarAkademik: string | null; + imageId: string; + tanggalMasuk: Date | null; + email: string | null; + telepon: string | null; + alamat: string | null; + posisiId: string; + aktif: boolean; + createdAt: Date; + updatedAt: Date; + }; + } | null, + + async load(id: string) { + try { + const res = await fetch(`/api/ekonomi/strukturorganisasi/hubunganorganisasi/${id}`); + const result = await res.json(); + + if (res.ok && result?.success) { + hubunganOrganisasi.findUnique.data = result.data; + } else { + hubunganOrganisasi.findUnique.data = null; + toast.error(result?.message || "Gagal mengambil data"); + } + } catch (error) { + console.error("Find unique error:", error); + hubunganOrganisasi.findUnique.data = null; + } + }, + }, + + edit: { + id: "", + form: { ...defaultHubunganOrganisasiForm }, + loading: false, + + async load(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + const res = await fetch(`/api/ekonomi/strukturorganisasi/hubunganorganisasi/${id}`); + const result = await res.json(); + + if (res.ok && result?.success) { + const data = result.data; + this.id = data.id; + this.form = { + atasanId: data.atasanId, + bawahanId: data.bawahanId, + tipe: data.tipe || "", + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); + } + } catch (error) { + console.error("Error loading:", error); + toast.error(error instanceof Error ? error.message : "Gagal memuat data"); + return null; + } + }, + + async update() { + const cek = templateHubunganOrganisasiForm.safeParse(this.form); + if (!cek.success) { + const err = `[${cek.error.issues + .map((v) => `${v.path.join(".")}: ${v.message}`) + .join("\n")}]`; + return toast.error(err); + } + + try { + this.loading = true; + const res = await fetch( + `/api/ekonomi/strukturorganisasi/hubunganorganisasi/${this.id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(this.form), + } + ); + + const result = await res.json(); + if (res.ok && result.success) { + await hubunganOrganisasi.findMany.load(); + toast.success("Berhasil mengupdate hubungan organisasi"); + return true; + } else { + throw new Error(result?.message || "Gagal mengupdate"); + } + } catch (error) { + console.error("Update error:", error); + toast.error(error instanceof Error ? error.message : "Gagal update"); + return false; + } finally { + this.loading = false; + } + }, + + reset() { + hubunganOrganisasi.edit.id = ""; + hubunganOrganisasi.edit.form = { ...defaultHubunganOrganisasiForm }; + }, + }, + + delete: { + loading: false, + async byId(id: string) { + if (!id) return toast.warn("ID tidak valid"); + + try { + hubunganOrganisasi.delete.loading = true; + const res = await fetch(`/api/strukturorganisasi/hubungan-organisasi/${id}`, { + method: "DELETE", + }); + + const result = await res.json(); + if (res.ok && result?.success) { + toast.success("Hubungan organisasi berhasil dihapus"); + hubunganOrganisasi.findMany.load(); + } else { + toast.error(result?.message || "Gagal menghapus hubungan organisasi"); + } + } catch (error) { + console.error("Delete error:", error); + toast.error("Terjadi kesalahan saat menghapus"); + } finally { + hubunganOrganisasi.delete.loading = false; + } + }, + }, +}); + + const strukturorganisasiState = proxy({ + posisiOrganisasi, + pegawai, + hubunganOrganisasi + }) + +export default strukturorganisasiState; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx new file mode 100644 index 00000000..fb8a133b --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx @@ -0,0 +1,67 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; + +function LayoutTabs({ children }: { children: React.ReactNode }) { + const router = useRouter() + const pathname = usePathname() + const tabs = [ + { + label: "Posisi Organisasi", + value: "posisiorganisasi", + href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi" + }, + { + label: "Pegawai", + value: "pegawai", + href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai" + }, + { + label: "Hubungan Organisasi", + value: "hubunganorganisasi", + href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi" + }, + ]; + const curentTab = tabs.find(tab => tab.href === pathname) + const [activeTab, setActiveTab] = useState(curentTab?.value || tabs[0].value); + + const handleTabChange = (value: string | null) => { + const tab = tabs.find(t => t.value === value) + if (tab) { + router.push(tab.href) + } + setActiveTab(value) + } + + useEffect(() => { + const match = tabs.find(tab => tab.href === pathname) + if (match) { + setActiveTab(match.value) + } + }, [pathname]) + + return ( + + Struktur Organisasi & SK Pengurus BUMDesa + + + {tabs.map((e, i) => ( + {e.label} + ))} + + {tabs.map((e, i) => ( + + {/* Konten dummy, bisa diganti tergantung routing */} + <> + + ))} + + {children} + + ); +} + +export default LayoutTabs; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/page.tsx similarity index 68% rename from src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/page.tsx rename to src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/page.tsx index b75a51c6..69da2f21 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi/page.tsx @@ -3,7 +3,7 @@ import React from 'react'; function Page() { return (
- struktur-organisasi-dan-sk-pengurus-bumdesa + Page
); } diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/layout.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/layout.tsx new file mode 100644 index 00000000..57a94a4f --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/layout.tsx @@ -0,0 +1,12 @@ +'use client' + +import LayoutTabs from "./_lib/layoutTabs" + + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx new file mode 100644 index 00000000..69da2f21 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function Page() { + return ( +
+ Page +
+ ); +} + +export default Page; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx new file mode 100644 index 00000000..60c73a29 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx @@ -0,0 +1,117 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; +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 EditPosisiOrganisasi() { + const router = useRouter(); + const params = useParams(); + const id = params?.id as string; + const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi); + + const [formData, setFormData] = useState({ + nama: "", + deskripsi: "", + hierarki: 0, + }); + + useEffect(() => { + const loadPosisiOrganisasi = async () => { + if (!id) return; + + try { + const data = await stateOrganisasi.edit.load(id); + + if (data) { + // pastikan id-nya masuk ke state edit + stateOrganisasi.edit.id = id; + setFormData({ + nama: data.nama || '', + deskripsi: data.deskripsi || '', + hierarki: data.hierarki || 0, + }); + } + } catch (error) { + console.error("Error loading posisi organisasi:", error); + toast.error("Gagal memuat data posisi organisasi"); + } + }; + + loadPosisiOrganisasi(); + }, [id]); + + const handleSubmit = async () => { + try { + if (!formData.nama.trim()) { + toast.error('Nama posisi organisasi tidak boleh kosong'); + return; + } + + stateOrganisasi.edit.form = { + nama: formData.nama.trim(), + deskripsi: formData.deskripsi.trim(), + hierarki: formData.hierarki, + }; + + // Safety check tambahan: pastikan ID tidak kosong + if (!stateOrganisasi.edit.id) { + stateOrganisasi.edit.id = id; // fallback + } + + const success = await stateOrganisasi.edit.update(); + + if (success) { + router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi"); + } + } catch (error) { + console.error("Error updating posisi organisasi:", error); + // toast akan ditampilkan dari fungsi update + } + }; + + return ( + + + + + + + + Edit Posisi Organisasi + setFormData({ ...formData, nama: e.target.value })} + label={Nama Posisi Organisasi} + placeholder='Masukkan nama posisi organisasi' + /> + { + setFormData({ ...formData, deskripsi: htmlContent }); + }} + /> + setFormData({ ...formData, hierarki: parseInt(e.target.value) })} + label={Hierarki} + placeholder='Masukkan hierarki' + /> + + + + + + + ); +} + +export default EditPosisiOrganisasi; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx new file mode 100644 index 00000000..dddcbe3f --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx @@ -0,0 +1,79 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; +import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; +import colors from '@/con/colors'; +import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; +import { useProxy } from 'valtio/utils'; + +function CreatePosisiOrganisasi() { + const router = useRouter(); + const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi) + useEffect(() => { + stateOrganisasi.findMany.load(); + }, []); + + const resetForm = () => { + stateOrganisasi.create.form = { + nama: "", + deskripsi: "", + hierarki: 0, // Initialize as 0 to allow any number input + }; + }; + + const handleSubmit = async () => { + await stateOrganisasi.create.submit(); + resetForm(); + router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi") + }; + + return ( + + + + + + + Create Posisi Organisasi + (stateOrganisasi.create.form.nama = e.currentTarget.value)} + /> + { + stateOrganisasi.create.form.deskripsi = htmlContent; + }} + /> + { + const value = parseInt(e.currentTarget.value, 10); + if (!isNaN(value)) { + stateOrganisasi.create.form.hierarki = value; + } + }} + /> + + + + + ); +} + +export default CreatePosisiOrganisasi; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx new file mode 100644 index 00000000..75ea5462 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx @@ -0,0 +1,119 @@ +'use client' +import colors from '@/con/colors'; +import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; +import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import HeaderSearch from '../../../_com/header'; +import JudulList from '../../../_com/judulList'; +import { useProxy } from 'valtio/utils'; +import { useState } from 'react'; +import { useShallowEffect } from '@mantine/hooks'; +import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi'; + +function PosisiOrganisasi() { + return ( + + } + /> + + + ); +} + +function ListPosisiOrganisasi() { + const stateOrganisasi = useProxy(strukturorganisasiState.posisiOrganisasi) + const router = useRouter(); + const [modalHapus, setModalHapus] = useState(false) + const [selectedId, setSelectedId] = useState(null) + + useShallowEffect(() => { + stateOrganisasi.findMany.load() + }, []) + + const handleHapus = async () => { + if (selectedId) { + await stateOrganisasi.delete.byId(selectedId); + setModalHapus(false) + setSelectedId(null) + } + } + + if (!stateOrganisasi.findMany.data) { + return ( + + + + ) + } + + return ( + + + + + + + Nama Posisi + Deskripsi + Hierarki + Edit + Hapus + + + + {stateOrganisasi.findMany.data?.map((item) => ( + + {item.nama} + + + + {item.hierarki} + + + + + + + + ))} + +
+
+ {/* Modal Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah anda yakin ingin menghapus posisi organisasi ini?" + /> +
+ ); +} + +export default PosisiOrganisasi; diff --git a/src/app/admin/_com/list_PageAdmin.tsx b/src/app/admin/_com/list_PageAdmin.tsx index bbdab223..1a8ff1f5 100644 --- a/src/app/admin/_com/list_PageAdmin.tsx +++ b/src/app/admin/_com/list_PageAdmin.tsx @@ -225,7 +225,7 @@ export const navBar = [ { id: "Ekonomi_3", name: "Struktur Organisasi dan SK Pengurus BUMDesa", - path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa" + path: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi" }, { id: "Ekonomi_4", diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/index.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/index.ts index 2526c4a0..e7a0b350 100644 --- a/src/app/api/[[...slugs]]/_lib/ekonomi/index.ts +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/index.ts @@ -3,6 +3,7 @@ import PasarDesa from "./pasar-desa"; import LowonganKerja from "./lowongan-kerja"; import ProgramKemiskinan from "./program-kemiskinan"; import KategoriProduk from "./pasar-desa/kategori-produk"; +import StrukturOrganisasi from "./struktur-organisasi"; const Ekonomi = new Elysia({ prefix: "/api/ekonomi", @@ -12,5 +13,6 @@ const Ekonomi = new Elysia({ .use(KategoriProduk) .use(LowonganKerja) .use(ProgramKemiskinan) +.use(StrukturOrganisasi) export default Ekonomi \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/create.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/create.ts new file mode 100644 index 00000000..db964952 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/create.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormCreateHubunganOrganisasi = { + atasanId: string; + bawahanId: string; + tipe?: string; +}; + +export default async function hubunganOrganisasiCreate(context: Context) { + const body = await context.body as FormCreateHubunganOrganisasi; + + // Validasi minimal + if (!body || !body.atasanId || !body.bawahanId) { + return { + success: false, + message: "atasanId dan bawahanId wajib diisi", + }; + } + + try { + const data = await prisma.hubunganOrganisasi.create({ + data: { + atasanId: body.atasanId, + bawahanId: body.bawahanId, + tipe: body.tipe, + }, + }); + + return { + success: true, + message: "Berhasil membuat hubungan organisasi", + data, + }; + } catch (error: any) { + if (error.code === "P2002") { + return { + success: false, + message: "Hubungan antara atasan dan bawahan sudah ada", + }; + } + + console.error("Error create hubungan organisasi:", error); + return { + success: false, + message: "Gagal membuat hubungan organisasi", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/del.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/del.ts new file mode 100644 index 00000000..c65bcf67 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/del.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function hubunganOrganisasiDelete(context: Context) { + const { id } = context.params as { id: string }; + + if (!id) { + return { + success: false, + message: "ID wajib diisi", + }; + } + + try { + const deleted = await prisma.hubunganOrganisasi.delete({ + where: { id }, + }); + + return { + success: true, + message: "Hubungan organisasi berhasil dihapus", + data: deleted, + }; + } catch (error: any) { + console.error("Error delete hubungan organisasi:", error); + return { + success: false, + message: "Gagal menghapus hubungan organisasi", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/findMany.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/findMany.ts new file mode 100644 index 00000000..4de4200d --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/findMany.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; + +export default async function hubunganOrganisasiFindMany() { + try { + const data = await prisma.hubunganOrganisasi.findMany({ + include: { + atasan: true, + bawahan: true, + }, + orderBy: { + atasanId: "asc", + }, + }); + + return { + success: true, + data, + }; + } catch (error: any) { + console.error("Error findMany hubungan organisasi:", error); + return { + success: false, + message: "Gagal mengambil data hubungan organisasi", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/findUnique.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/findUnique.ts new file mode 100644 index 00000000..d8d16cba --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/findUnique.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function hubunganOrganisasiFindUnique(context: Context) { + const { id } = context.params as { id: string }; + + if (!id) { + return { + success: false, + message: "ID hubungan organisasi wajib diisi", + }; + } + + try { + const data = await prisma.hubunganOrganisasi.findUnique({ + where: { id }, + include: { + atasan: true, + bawahan: true, + }, + }); + + if (!data) { + return { + success: false, + message: "Data hubungan organisasi tidak ditemukan", + }; + } + + return { + success: true, + data, + }; + } catch (error: any) { + console.error("Error findUnique hubungan organisasi:", error); + return { + success: false, + message: "Gagal mengambil data", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/index.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/index.ts new file mode 100644 index 00000000..56c1440e --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/index.ts @@ -0,0 +1,48 @@ +import Elysia, { t } from "elysia"; +import hubunganOrganisasiFindMany from "./findMany"; +import hubunganOrganisasiFindUnique from "./findUnique"; +import hubunganOrganisasiCreate from "./create"; +import hubunganOrganisasiUpdate from "./updt"; +import hubunganOrganisasiDelete from "./del"; + + +const HubunganOrganisasi = new Elysia({ + prefix: "/hubungan-organisasi", + tags: ["Ekonomi/Struktur Organisasi/Hubungan Organisasi"], +}) + + // 🔍 GET /find-many + .get("/find-many", hubunganOrganisasiFindMany) + + // 🔍 GET /:id + .get("/:id", async (context) => { + return await hubunganOrganisasiFindUnique(context); + }) + + // ➕ POST /create + .post("/create", hubunganOrganisasiCreate, { + body: t.Object({ + atasanId: t.String(), + bawahanId: t.String(), + tipe: t.Optional(t.String()), + }), + }) + + // ✏️ PUT /:id + .put( "/:id", + async (context) => { + const response = await hubunganOrganisasiUpdate(context); + return response; + }, { + body: t.Object({ + id: t.String(), + atasanId: t.Optional(t.String()), + bawahanId: t.Optional(t.String()), + tipe: t.Optional(t.String()), + }), + }) + + // ❌ DELETE /:id + .delete("/:id", hubunganOrganisasiDelete); + +export default HubunganOrganisasi; diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/updt.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/updt.ts new file mode 100644 index 00000000..0ec8abd6 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/hubungan-organisasi/updt.ts @@ -0,0 +1,52 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormUpdateHubungan = { + id: string; + atasanId?: string; + bawahanId?: string; + tipe?: string; +}; + +export default async function hubunganOrganisasiUpdate(context: Context) { + const body = await context.body as FormUpdateHubungan; + + if (!body?.id) { + return { + success: false, + message: "ID wajib diisi untuk update", + }; + } + + try { + const updated = await prisma.hubunganOrganisasi.update({ + where: { id: body.id }, + data: { + atasanId: body.atasanId, + bawahanId: body.bawahanId, + tipe: body.tipe, + }, + }); + + return { + success: true, + message: "Hubungan organisasi berhasil diupdate", + data: updated, + }; + } catch (error: any) { + if (error.code === "P2002") { + return { + success: false, + message: "Relasi atasan-bawahan sudah ada", + }; + } + + console.error("Error update hubungan organisasi:", error); + return { + success: false, + message: "Gagal update data hubungan organisasi", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/index.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/index.ts new file mode 100644 index 00000000..b0ae7175 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/index.ts @@ -0,0 +1,14 @@ +import Elysia from "elysia"; +import PosisiOrganisasi from "./posisi-organisasi"; +import Pegawai from "./pegawai"; +import HubunganOrganisasi from "./hubungan-organisasi"; + +const StrukturOrganisasi = new Elysia({ + prefix: "/struktur-organisasi", + tags: ["Ekonomi/Struktur Organisasi"], +}) +.use(PosisiOrganisasi) +.use(Pegawai) +.use(HubunganOrganisasi) + +export default StrukturOrganisasi; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/create.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/create.ts new file mode 100644 index 00000000..05931573 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/create.ts @@ -0,0 +1,61 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormCreatePegawai = { + namaLengkap: string; + gelarAkademik?: string; + imageId: string; + tanggalMasuk?: string; // Kirim dari frontend dalam format ISO (ex: '2025-07-04') + email?: string; + telepon?: string; + alamat?: string; + posisiId: string; +}; + +export default async function pegawaiCreate(context: Context) { + const body = await context.body as FormCreatePegawai; + + if (!body || !body.namaLengkap || !body.posisiId) { + return { + success: false, + message: "namaLengkap dan posisiId wajib diisi", + }; + } + + try { + const pegawai = await prisma.pegawai.create({ + data: { + namaLengkap: body.namaLengkap, + gelarAkademik: body.gelarAkademik, + imageId: body.imageId, + tanggalMasuk: body.tanggalMasuk ? new Date(body.tanggalMasuk) : undefined, + email: body.email, + telepon: body.telepon, + alamat: body.alamat, + posisiId: body.posisiId, + // aktif, createdAt, updatedAt otomatis by Prisma default + }, + }); + + return { + success: true, + message: "Berhasil menambahkan pegawai", + data: pegawai, + }; + } catch (error: any) { + if (error.code === "P2002") { + return { + success: false, + message: "Email sudah digunakan", + }; + } + + console.error("Gagal menambahkan pegawai:", error); + return { + success: false, + message: "Terjadi kesalahan saat membuat pegawai", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/del.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/del.ts new file mode 100644 index 00000000..2059cdda --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/del.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function pegawaiDelete(context: Context) { + const { id } = context.params as { id: string }; + + if (!id) { + return { + success: false, + message: "ID pegawai tidak ditemukan", + }; + } + + try { + const deleted = await prisma.pegawai.update({ + where: { id }, + data: { + aktif: false, // soft delete + updatedAt: new Date(), + }, + }); + + return { + success: true, + message: "Pegawai berhasil di-nonaktifkan", + data: deleted, + }; + } catch (error: any) { + console.error("Error delete pegawai:", error); + return { + success: false, + message: "Gagal menghapus pegawai", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/findMany.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/findMany.ts new file mode 100644 index 00000000..f2d9b568 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/findMany.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; + +export default async function pegawaiFindMany() { + try { + const pegawaiList = await prisma.pegawai.findMany({ + where: { aktif: true }, // hanya yang aktif + orderBy: { createdAt: "desc" }, + include: { + posisi: true, + image: true, + }, + }); + + return { + success: true, + data: pegawaiList, + }; + } catch (error: any) { + console.error("Error findMany pegawai:", error); + return { + success: false, + message: "Gagal mengambil data pegawai", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/findUnique.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/findUnique.ts new file mode 100644 index 00000000..4c2c4884 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/findUnique.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function pegawaiFindUnique(context: Context) { + const { id } = context.params as { id: string }; + + if (!id) { + return { + success: false, + message: "ID pegawai diperlukan", + }; + } + + try { + const pegawai = await prisma.pegawai.findUnique({ + where: { id }, + include: { + posisi: true, + image: true + }, + }); + + if (!pegawai) { + return { + success: false, + message: "Pegawai tidak ditemukan", + }; + } + + return { + success: true, + data: pegawai, + }; + } catch (error: any) { + console.error("Error findUnique pegawai:", error); + return { + success: false, + message: "Gagal mengambil data pegawai", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/index.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/index.ts new file mode 100644 index 00000000..30e2daa4 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/index.ts @@ -0,0 +1,62 @@ +import Elysia, { t } from "elysia"; +import pegawaiFindMany from "./findMany"; +import pegawaiFindUnique from "./findUnique"; +import pegawaiCreate from "./create"; +import pegawaiDelete from "./del"; +import pegawaiUpdate from "./updt"; + +const Pegawai = new Elysia({ + prefix: "/pegawai", + tags: ["Ekonomi/Struktur Organisasi/Pegawai"], +}) + + // ✅ Find all + .get("/find-many", pegawaiFindMany) + + // ✅ Find by ID + .get("/:id", async (context) => { + const response = await pegawaiFindUnique(context); + return response; + }) + + // ✅ Create + .post("/create", pegawaiCreate, { + body: t.Object({ + namaLengkap: t.String(), + gelarAkademik: t.Optional(t.String()), + imageId: t.String(), + tanggalMasuk: t.Optional(t.String()), // ISO string (YYYY-MM-DD) + email: t.Optional(t.String()), + telepon: t.Optional(t.String()), + alamat: t.Optional(t.String()), + posisiId: t.String(), + }), + }) + + // ✅ Update + .put( + "/:id", + async (context) => { + const response = await pegawaiUpdate(context); + return response; + }, + { + body: t.Object({ + id: t.String(), + namaLengkap: t.Optional(t.String()), + gelarAkademik: t.Optional(t.String()), + imageId: t.String(), + tanggalMasuk: t.Optional(t.String()), + email: t.Optional(t.String()), + telepon: t.Optional(t.String()), + alamat: t.Optional(t.String()), + posisiId: t.Optional(t.String()), + aktif: t.Optional(t.Boolean()), + }), + } + ) + + // ✅ Delete + .delete("/:id", pegawaiDelete); + +export default Pegawai; diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/updt.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/updt.ts new file mode 100644 index 00000000..16a6c4dc --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/pegawai/updt.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormUpdatePegawai = { + id: string; + namaLengkap?: string; + gelarAkademik?: string; + imageId: string; + tanggalMasuk?: string; + email?: string; + telepon?: string; + alamat?: string; + posisiId?: string; + aktif?: boolean; +}; + +export default async function pegawaiUpdate(context: Context) { + const body = await context.body as FormUpdatePegawai; + + if (!body?.id) { + return { + success: false, + message: "ID pegawai wajib diisi", + }; + } + + try { + const updated = await prisma.pegawai.update({ + where: { id: body.id }, + data: { + namaLengkap: body.namaLengkap, + gelarAkademik: body.gelarAkademik, + imageId: body.imageId, + tanggalMasuk: body.tanggalMasuk ? new Date(body.tanggalMasuk) : undefined, + email: body.email, + telepon: body.telepon, + alamat: body.alamat, + posisiId: body.posisiId, + aktif: body.aktif, + updatedAt: new Date(), + }, + }); + + return { + success: true, + message: "Pegawai berhasil diperbarui", + data: updated, + }; + } catch (error: any) { + console.error("Error update pegawai:", error); + return { + success: false, + message: "Gagal memperbarui data pegawai", + error: error.message, + }; + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/create.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/create.ts new file mode 100644 index 00000000..10b3a994 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/create.ts @@ -0,0 +1,37 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormCreate = { + nama: string; + deskripsi: string; + hierarki: number; +} + +export default async function posisiOrganisasiCreate(context: Context) { + const body = context.body as FormCreate; + + if(!body) { + return { + success: false, + message: "Body is required", + }; + } + + try { + const posisiOrganisasi = await prisma.posisiOrganisasi.create({ + data: { + nama: body.nama, + deskripsi: body.deskripsi, + hierarki: body.hierarki, + }, + }); + return { + success: true, + message: "Success create posisi organisasi", + data: posisiOrganisasi + }; + } catch (error) { + console.error("Error creating PosisiOrganisasi:", error); + throw new Error("Failed to create PosisiOrganisasi: " + (error as Error).message); + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/del.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/del.ts new file mode 100644 index 00000000..0560b115 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/del.ts @@ -0,0 +1,91 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function posisiOrganisasiDelete(context: Context) { + const { id } = context.params as { id: string }; + + if (!id) { + return new Response( + JSON.stringify({ + success: false, + message: "ID wajib diisi", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + try { + // Check if the position exists first + const existing = await prisma.posisiOrganisasi.findUnique({ + where: { id }, + }); + + if (!existing) { + return new Response( + JSON.stringify({ + success: false, + message: "Posisi organisasi tidak ditemukan", + }), + { status: 404, headers: { "Content-Type": "application/json" } } + ); + } + + // Check if there are any pegawai associated with this position + const pegawaiCount = await prisma.pegawai.count({ + where: { posisiId: id }, + }); + + if (pegawaiCount > 0) { + return new Response( + JSON.stringify({ + success: false, + message: "Tidak dapat menghapus posisi yang masih memiliki pegawai", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // Check if this position is used in any hubungan organisasi + const hubunganCount = await prisma.hubunganOrganisasi.count({ + where: { + OR: [ + { atasanId: id }, + { bawahanId: id }, + ], + }, + }); + + if (hubunganCount > 0) { + return new Response( + JSON.stringify({ + success: false, + message: "Tidak dapat menghapus posisi yang masih terdaftar dalam struktur organisasi", + }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + + // If all checks pass, delete the position + const deleted = await prisma.posisiOrganisasi.delete({ + where: { id }, + }); + + return new Response( + JSON.stringify({ + success: true, + message: "Posisi organisasi berhasil dihapus", + data: deleted, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } catch (error) { + console.error("Error delete posisi organisasi:", error); + return new Response( + JSON.stringify({ + success: false, + message: "Terjadi kesalahan saat menghapus posisi organisasi", + }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +} diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/findMany.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/findMany.ts new file mode 100644 index 00000000..2b0263ab --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/findMany.ts @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import prisma from "@/lib/prisma"; + +export default async function posisiOrganisasiFindMany() { + const data = await prisma.posisiOrganisasi.findMany(); + + return { + success: true, + message: "Berhasil mengambil semua data posisi organisasi", + data: data.map((item: any) => ({ + id: item.id, + nama: item.nama, + deskripsi: item.deskripsi, + hierarki: item.hierarki, + })), + }; +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/findUnique.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/findUnique.ts new file mode 100644 index 00000000..13092469 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/findUnique.ts @@ -0,0 +1,47 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +export default async function posisiOrganisasiFindUnique(context: Context) { + const url = new URL(context.request.url); + const pathSegments = url.pathname.split('/'); + const id = pathSegments[pathSegments.length - 1]; + + if (!id) { + return { + success: false, + message: "ID is required", + } + } + + try { + if (typeof id !== 'string') { + return { + success: false, + message: "ID is required", + } + } + + const data = await prisma.posisiOrganisasi.findUnique({ + where: { id }, + }); + + if (!data) { + return { + success: false, + message: "Posisi organisasi tidak ditemukan", + } + } + + return { + success: true, + message: "Success find posisi organisasi", + data, + } + } catch (error) { + console.error("Find by ID error:", error); + return { + success: false, + message: "Gagal mengambil posisi organisasi: " + (error instanceof Error ? error.message : 'Unknown error'), + } + } +} \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/index.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/index.ts new file mode 100644 index 00000000..4338204d --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/index.ts @@ -0,0 +1,37 @@ +import Elysia, { t } from "elysia"; +import posisiOrganisasiFindMany from "./findMany"; +import posisiOrganisasiFindUnique from "./findUnique"; +import posisiOrganisasiCreate from "./create"; +import posisiOrganisasiUpdate from "./updt"; +import posisiOrganisasiDelete from "./del"; + +const PosisiOrganisasi = new Elysia({ + prefix: "/posisi-organisasi", + tags: ["Ekonomi/Struktur Organisasi/Posisi Organisasi"], +}) + +.get("/find-many", posisiOrganisasiFindMany) +.get("/:id", async (context) => { + const response = await posisiOrganisasiFindUnique(context); + return response; +}) +.post("/create", posisiOrganisasiCreate, { + body: t.Object({ + nama: t.String(), + deskripsi: t.String(), + hierarki: t.Number(), + }), +}) +.put("/:id", async (context) => { + const response = await posisiOrganisasiUpdate(context); + return response; +}, { + body: t.Object({ + nama: t.String(), + deskripsi: t.String(), + hierarki: t.Number(), + }), +}) +.delete("/del/:id", posisiOrganisasiDelete); + +export default PosisiOrganisasi; \ No newline at end of file diff --git a/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/updt.ts b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/updt.ts new file mode 100644 index 00000000..e3639460 --- /dev/null +++ b/src/app/api/[[...slugs]]/_lib/ekonomi/struktur-organisasi/posisi-organisasi/updt.ts @@ -0,0 +1,49 @@ +import prisma from "@/lib/prisma"; +import { Context } from "elysia"; + +type FormUpdate = { + id: string; + nama: string; + deskripsi: string; + hierarki: number; +}; + +export default async function posisiOrganisasiUpdate(context: Context) { + const body = context.body as FormUpdate; + const id = context.params?.id as string; + + if (!id) { + return { + success: false, + message: "ID is required", + }; + } + + try { + await prisma.posisiOrganisasi.update({ + where: { id }, + data: { + nama: body.nama, + deskripsi: body.deskripsi, + hierarki: body.hierarki, + }, + }); + + const updated = await prisma.posisiOrganisasi.findUnique({ + where: { id }, + }); + + return { + success: true, + message: "Success update posisi organisasi", + data: updated, + }; + } catch (error) { + console.error("Update error:", error); + return { + success: false, + message: "Gagal update posisi organisasi", + error: error instanceof Error ? error.message : String(error), + }; + } +}