UI & API Menu PPID, Submenu Struktur PPID

This commit is contained in:
2025-07-31 16:22:02 +08:00
parent 4e61695649
commit 024d5517fa
62 changed files with 3790 additions and 712 deletions

View File

@@ -1,169 +1,683 @@
/* 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";
const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
})
name: z.string().min(3, "Nama minimal 3 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
});
const defaultForm = {
name: "",
imageId: "",
name: "",
imageId: "",
};
type StrukturPPIDForm = Prisma.StrukturPPIDGetPayload<{
select: {
id: true;
name: true;
imageId: true;
image?: {
select: {
link: true;
};
};
select: {
id: true;
name: true;
imageId: true;
image?: {
select: {
link: true;
};
};
};
}>;
const stateStrukturPPID = proxy({
struktur: {
data: null as StrukturPPIDForm | null,
loading: false,
error: null as string | null,
const stateStruktur = proxy({
struktur: {
data: null as StrukturPPIDForm | null,
loading: false,
error: null as string | null,
async load(id: string) {
if(!id) {
toast.warn("ID tidak valid")
return null
}
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
this.loading = true;
this.error = null;
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ppid/strukturppid/${id}`);
if(!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
try {
const response = await fetch(`/api/ppid/strukturppid/${id}`);
const result = await response.json();
if(result.success) {
this.data = result.data;
return result.data
} else {
throw new Error(result.message || "Gagal mengambil data struktur")
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Load struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data struktur");
return null;
} finally {
this.loading = false;
}
},
reset() {
this.data = null;
this.error = null;
this.loading = false;
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
},
editStruktur: {
id: "",
form: { ...defaultForm },
loading: false,
error: null as string | null,
isReadOnly: false,
const result = await response.json();
initialize(strukturData: StrukturPPIDForm) {
this.id = strukturData.id;
this.isReadOnly = false;
this.form = {
name: strukturData.name || "",
imageId: strukturData.imageId || "",
};
},
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
async submit() {
const validation = templateForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ppid/strukturppid/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
})
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 struktur");
await stateStrukturPPID.struktur.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update struktur");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat update struktur");
return false;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...defaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
if (result.success) {
this.data = result.data;
return result.data;
} else {
throw new Error(result.message || "Gagal mengambil data struktur");
}
},
async loadForEdit(id: string) {
const strukturData = await this.struktur.load(id);
if (strukturData) {
this.editStruktur.initialize(strukturData);
}
return strukturData;
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Load struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data struktur");
return null;
} finally {
this.loading = false;
}
},
reset() {
this.struktur.reset();
this.editStruktur.reset();
this.data = null;
this.error = null;
this.loading = false;
},
},
editStruktur: {
id: "",
form: { ...defaultForm },
loading: false,
error: null as string | null,
isReadOnly: false,
initialize(strukturData: StrukturPPIDForm) {
this.id = strukturData.id;
this.isReadOnly = false;
this.form = {
name: strukturData.name || "",
imageId: strukturData.imageId || "",
};
},
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
async submit() {
const validation = templateForm.safeParse(this.form);
if (!validation.success) {
const errors = validation.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false;
}
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/ppid/strukturppid/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
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 struktur");
await stateStruktur.struktur.load(this.id);
return true;
} else {
throw new Error(result.message || "Gagal update struktur");
}
} catch (error) {
const errorMessage = (error as Error).message;
this.error = errorMessage;
console.error("Update struktur error:", errorMessage);
toast.error("Terjadi kesalahan saat update struktur");
return false;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...defaultForm };
this.error = null;
this.loading = false;
this.isReadOnly = false;
},
},
async loadForEdit(id: string) {
const strukturData = await this.struktur.load(id);
if (strukturData) {
this.editStruktur.initialize(strukturData);
}
})
return strukturData;
},
reset() {
this.struktur.reset();
this.editStruktur.reset();
},
});
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.ppid.strukturppid.posisiorganisasi[
"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 };
},
},
findUnique: {
data: null as Prisma.StrukturOrganisasiPPIDGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ppid/strukturppid/posisiorganisasi/${id}`
);
if (res.ok) {
const data = await res.json();
posisiOrganisasi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch posisiOrganisasi:", res.statusText);
posisiOrganisasi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching posisiOrganisasi:", error);
posisiOrganisasi.findUnique.data = null;
}
},
},
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/ppid/strukturppid/posisiorganisasi/${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/ppid/strukturppid/posisiorganisasi/${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.ppid.strukturppid.posisiorganisasi[
"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/ppid/strukturppid/posisiorganisasi/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().min(1, "Gelar Akademik wajib diisi"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"), // ISO format
email: z.string().email("Email tidak valid").optional(),
telepon: z.string().min(1, "Telepom wajib diisi"),
alamat: z.string().min(1, "Alamat wajib diisi"),
posisiId: z.string().min(1, "Posisi wajib diisi"),
isActive: z.boolean().default(true),
});
const pegawaiDefaultForm = {
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
};
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.ppid.strukturppid.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;
}
},
},
// In struktur-organisasi.ts
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
pegawai.findMany.loading = true; // Use the full path to access the property
pegawai.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
pegawai.findMany.data = res.data.data || [];
pegawai.findMany.total = res.data.total || 0;
pegawai.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load pegawai:", res.data?.message);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pegawai:", error);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
} finally {
pegawai.findMany.loading = false;
}
},
},
findUnique: {
data: null as
| (Prisma.PegawaiGetPayload<{
include: { posisi: true; image: true };
}> & { isActive: boolean })
| null,
async load(id: string) {
const res = await fetch(`/api/ppid/strukturppid/pegawai/${id}`);
if (res.ok) {
const json = await res.json();
pegawai.findUnique.data = json.data
? {
...json.data,
isActive: json.data.isActive ?? json.data.aktif ?? true, // Fallback ke aktif:true jika tidak ada 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/ppid/strukturppid/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) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/ppid/strukturppid/pegawai/${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 = {
namaLengkap: data.namaLengkap,
gelarAkademik: data.gelarAkademik,
imageId: data.imageId,
tanggalMasuk: data.tanggalMasuk,
email: data.email,
telepon: data.telepon,
alamat: data.alamat,
posisiId: data.posisiId,
isActive: data.isActive,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const cek = templatePegawai.safeParse(pegawai.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pegawai.edit.loading = true;
// Format tanggalMasuk to ISO string if it exists
const formattedTanggalMasuk = this.form.tanggalMasuk
? new Date(this.form.tanggalMasuk).toISOString()
: undefined;
const response = await fetch(
`/api/ppid/strukturppid/pegawai/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: this.id,
namaLengkap: this.form.namaLengkap,
gelarAkademik: this.form.gelarAkademik,
imageId: this.form.imageId || null,
tanggalMasuk: formattedTanggalMasuk,
email: this.form.email,
telepon: this.form.telepon,
alamat: this.form.alamat,
posisiId: this.form.posisiId,
isActive: this.form.isActive,
}),
}
);
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 pegawai");
await pegawai.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update pegawai"
);
return false;
} finally {
pegawai.edit.loading = false;
}
},
reset() {
pegawai.edit.id = "";
pegawai.edit.form = { ...pegawaiDefaultForm };
},
},
});
const stateStrukturPPID = proxy({
stateStruktur,
posisiOrganisasi,
pegawai
});
export default stateStrukturPPID;

View File

@@ -1,220 +1,456 @@
import { proxy } from "valtio";
import { toast } from "react-toastify";
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { z } from "zod";
import { proxy } from 'valtio'
import { toast } from 'react-toastify'
import ApiFetch from '@/lib/api-fetch'
import { Prisma } from '@prisma/client'
import { z } from 'zod'
// 1. Validasi Zod
const userSchema = z.object({
nama: z.string().min(1, "Nama harus diisi"),
email: z.string().email("Email tidak valid"),
password: z.string().min(6, "Password minimal 6 karakter"),
nama: z.string().min(1, 'Nama harus diisi'),
email: z.string().email('Email tidak valid'),
password: z.string().min(6, 'Password minimal 6 karakter'),
roleId: z.string().optional(),
});
})
const defaultForm = {
nama: "",
email: "",
password: "",
roleId: "",
};
const defaultForm = { nama: '', email: '', password: '', roleId: '' }
// 2. State Valtio
const userState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create(isAdmin: boolean = false) {
const valid = userSchema.safeParse(userState.create.form);
if (!valid.success) {
const err = valid.error.issues.map((i) => i.message).join(", ");
return toast.error(err);
}
try {
userState.create.loading = true;
const res = await ApiFetch.api.user[
isAdmin ? "create" : "register"
].post(userState.create.form);
if (res.status === 200) {
toast.success("User berhasil dibuat");
userState.findMany.load();
} else {
toast.error(res.data?.message || "Gagal membuat user");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat membuat user");
} finally {
userState.create.loading = false;
}
},
},
login: {
form: { email: "", password: "" },
loading: false,
async submit() {
try {
userState.login.loading = true;
const res = await ApiFetch.api.user.login.post(userState.login.form);
if (res.status === 200) {
toast.success("Login berhasil");
const token = res.data?.data?.token;
if (typeof token === "string") {
localStorage.setItem("token", token);
}
} else {
toast.error(res.data?.message || "Login gagal");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat login");
} finally {
userState.login.loading = false;
}
},
},
// Register
register: {
form: { ...defaultForm },
loading: false,
async submit() {
const valid = userSchema.safeParse(userState.register.form);
const valid = userSchema.omit({ roleId: true }).safeParse(userState.register.form)
if (!valid.success) {
const err = valid.error.issues.map(i => i.message).join(", ");
return toast.error(err);
const err = valid.error.issues.map(i => i.message).join(', ')
return toast.error(err)
}
try {
userState.register.loading = true;
const res = await ApiFetch.api.user.register.post(userState.register.form);
userState.register.loading = true
const res = await ApiFetch.api.user.register.post(userState.register.form)
if (res.status === 200) {
toast.success("Registrasi berhasil, silakan login");
userState.register.form = { ...defaultForm }; // Reset form
toast.success('Registrasi berhasil, silakan login')
userState.register.form = { ...defaultForm } // reset
} else {
toast.error(res.data?.message || "Gagal registrasi");
toast.error(res.data?.message || 'Gagal registrasi')
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat registrasi");
console.error(e)
toast.error('Terjadi kesalahan saat registrasi')
} finally {
userState.register.loading = false;
userState.register.loading = false
}
},
},
// Login
login: {
form: { email: '', password: '' },
loading: false,
async submit() {
try {
userState.login.loading = true
const res = await ApiFetch.api.user.login.post(userState.login.form)
if (res.status === 200) {
toast.success('Login berhasil')
const token = res.data?.data?.token
if (typeof token === 'string') {
localStorage.setItem('token', token)
// Optional: simpan user role untuk otorisasi
const user = res.data?.data?.user
localStorage.setItem('user', JSON.stringify(user))
}
} else {
toast.error(res.data?.message || 'Login gagal')
}
} catch (e) {
console.error(e)
toast.error('Terjadi kesalahan saat login')
} finally {
userState.login.loading = false
}
},
},
// CRUD User (untuk admin)
create: {
form: { ...defaultForm },
loading: false,
async create(isAdmin = false) {
const valid = userSchema.safeParse(userState.create.form)
if (!valid.success) {
const err = valid.error.issues.map(i => i.message).join(', ')
return toast.error(err)
}
try {
userState.create.loading = true
const endpoint = isAdmin ? 'create' : 'register'
const res = await ApiFetch.api.user[endpoint].post(userState.create.form)
if (res.status === 200) {
toast.success('User berhasil dibuat')
userState.findMany.load() // refresh list
userState.create.form = { ...defaultForm } // reset form
} else {
toast.error(res.data?.message || 'Gagal membuat user')
}
} catch (e) {
console.error(e)
toast.error('Gagal membuat user')
} finally {
userState.create.loading = false
}
},
},
// Find Many
findMany: {
data: [] as Prisma.UserGetPayload<{ include: { role: true } }>[],
loading: false,
async load() {
userState.findMany.loading = true;
const res = await ApiFetch.api.user.findMany.get();
if (res.status === 200) {
userState.findMany.data = res.data?.data ?? [];
this.loading = true
try {
const res = await ApiFetch.api.user.findMany.get()
if (res.status === 200) {
this.data = res.data?.data || []
}
} catch (e) {
console.error(e)
toast.error('Gagal muat data user')
} finally {
this.loading = false
}
userState.findMany.loading = false;
},
},
// Find Unique
findUnique: {
data: null as Prisma.UserGetPayload<{ include: { role: true } }> | null,
loading: false,
async load(id: string) {
this.loading = true
try {
userState.findUnique.loading = true;
const res = await fetch(`/api/user/findUnique/${id}`);
const data = await res.json();
const res = await fetch(`/api/user/findUnique/${id}`)
const data = await res.json()
if (res.status === 200) {
userState.findUnique.data = data.data ?? null;
this.data = data.data
} else {
toast.error(data.message)
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat mengambil data user");
console.error(e)
toast.error('Gagal ambil data user')
} finally {
userState.findUnique.loading = false;
this.loading = false
}
},
},
// Update
update: {
id: "",
id: '',
form: { ...defaultForm },
loading: false,
async load(id: string) {
this.loading = true
try {
userState.update.loading = true;
const res = await fetch(`/api/user/findUnique/${id}`);
const data = await res.json();
const res = await fetch(`/api/user/findUnique/${id}`)
const data = await res.json()
if (res.status === 200) {
const user = data.data;
userState.update.id = user.id;
userState.update.form = {
const user = data.data
this.id = user.id
this.form = {
nama: user.nama,
email: user.email,
password: "",
password: '', // jangan kirim password lama
roleId: user.roleId,
};
}
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat mengambil data user");
console.error(e)
toast.error('Gagal muat data')
} finally {
userState.update.loading = false;
this.loading = false
}
},
async submit() {
this.loading = true
try {
userState.update.loading = true;
const res = await fetch(`/api/user/update/${userState.update.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(userState.update.form),
});
const data = await res.json();
const res = await fetch(`/api/user/update/${this.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form),
})
const data = await res.json()
if (res.status === 200) {
toast.success("Berhasil update user");
userState.findMany.load();
toast.success('Update berhasil')
userState.findMany.load()
} else {
toast.error(data?.message || "Gagal update user");
toast.error(data.message || 'Gagal update')
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat update user");
console.error(e)
toast.error('Gagal update user')
} finally {
userState.update.loading = false;
this.loading = false
}
},
},
// Delete (Soft Delete)
delete: {
loading: false,
async submit(id: string) {
this.loading = true
try {
const res = await fetch(`/api/user/del/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
})
const data = await res.json()
if (res.status === 200) {
toast.success('User dinonaktifkan')
userState.findMany.load()
} else {
toast.error(data.message || 'Gagal hapus')
}
} catch (e) {
console.error(e)
toast.error('Gagal hapus user')
} finally {
this.loading = false
}
},
},
})
const templateRole = z.object({
name: z.string().min(1, "Nama harus diisi"),
});
const defaultRole = {
name: "",
};
const roleState = proxy({
create: {
form: { ...defaultRole },
loading: false,
async create() {
const cek = templateRole.safeParse(roleState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
roleState.create.loading = true;
const res =
await ApiFetch.api.role[
"create"
].post(roleState.create.form);
if (res.status === 200) {
roleState.findMany.load();
return toast.success("Data role Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
roleState.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.RoleGetPayload<{
omit: {
isActive: true;
};
}>[],
loading: false,
async load() {
const res =
await ApiFetch.api.role[
"findMany"
].get();
if (res.status === 200) {
roleState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.RoleGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/role/${id}`
);
if (res.ok) {
const data = await res.json();
roleState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
roleState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
roleState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async submit(id: string) {
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
userState.delete.loading = true;
const res = await fetch(`/api/user/del/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
const data = await res.json();
if (res.status === 200) {
toast.success("User berhasil dihapus");
userState.findMany.load();
roleState.delete.loading = true;
const response = await fetch(
`/api/role/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Data role berhasil dihapus"
);
await roleState.findMany.load(); // refresh list
} else {
toast.error(data?.message || "Gagal hapus user");
toast.error(result?.message || "Gagal menghapus Data role");
}
} catch (e) {
console.error(e);
toast.error("Terjadi kesalahan saat hapus user");
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data role");
} finally {
userState.delete.loading = false;
roleState.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultRole },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/role/${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,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading role:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateRole.safeParse(roleState.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
roleState.update.loading = true;
const response = await fetch(
`/api/role/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
}),
}
);
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 data role");
await roleState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update data role");
}
} catch (error) {
console.error("Error updating data role:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data role"
);
return false;
} finally {
roleState.update.loading = false;
}
},
reset() {
roleState.update.id = "";
roleState.update.form = { ...defaultRole };
},
},
});
export default userState;
const user = proxy({
userState,
roleState,
})
export default user

View File

@@ -126,7 +126,7 @@ export default function EditPegawai() {
const success = await stateOrganisasi.edit.submit();
if (success) {
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);

View File

@@ -25,7 +25,7 @@ function DetailPegawai() {
statePegawai.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai")
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai")
}
}
@@ -120,7 +120,7 @@ function DetailPegawai() {
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
}
}}
disabled={!statePegawai.findUnique.data}

View File

@@ -64,7 +64,7 @@ function CreatePegawai() {
// Reset form dan redirect
resetForm();
toast.success("Data pegawai berhasil ditambahkan");
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai");
} catch (error) {
console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");

View File

@@ -78,7 +78,7 @@ function ListPegawai({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
<JudulList
title='List Pegawai'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create'
href='/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
@@ -131,7 +131,7 @@ function ListPegawai({ search }: { search: string }) {
</Group>
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/pegawai/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>

View File

@@ -68,7 +68,7 @@ function EditPosisiOrganisasi() {
const success = await stateOrganisasi.edit.update();
if (success) {
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi");
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi");
}
} catch (error) {
console.error("Error updating posisi organisasi:", error);

View File

@@ -27,7 +27,7 @@ function CreatePosisiOrganisasi() {
const handleSubmit = async () => {
await stateOrganisasi.create.submit();
resetForm();
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi")
router.push("/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi")
};
return (

View File

@@ -67,7 +67,7 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Posisi Organisasi'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create'
href='/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
@@ -91,7 +91,7 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
<Button color="green"
onClick={() => {
if (item) {
router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/${item.id}`);
router.push(`/admin/ekonomi/Struktur-Organisasi-dan-SK-Pengurus-BUMDesa/posisi-organisasi/${item.id}`);
}
}}
>

View File

@@ -68,6 +68,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
title='List Kategori Kegiatan'
href='/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi/create'
/>
<Box style={{overflowY: "auto"}}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
@@ -97,6 +98,7 @@ function ListKategoriKegiatan({ search }: { search: string }) {
))}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus

View File

@@ -68,35 +68,37 @@ function ListKategoriPrestasi({ search }: { search: string }) {
title='List Kategori Prestasi Desa'
href='/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
<Box style={{ overflowY: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Kategori</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${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>
</Box>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus

View File

@@ -57,7 +57,8 @@ function ListMediaSosial({ search }: { search: string }) {
title='List Media Sosial'
href='/admin/landing-page/profile/media-sosial/create'
/>
<Table striped withTableBorder withRowBorders>
<Box style={{overflowY: "auto"}}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Media Sosial / Nama Kontak</TableTh>
@@ -85,6 +86,7 @@ function ListMediaSosial({ search }: { search: string }) {
))}
</TableTbody>
</Table>
</Box>
</Paper>
</Box>
);

View File

@@ -58,30 +58,32 @@ function ListProgramInovasi({ search }: { search: string }) {
title='List Program Inovasi'
href='/admin/landing-page/profile/program-inovasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.description}</TableTd>
<TableTd>{item.link}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Box style={{ overflowY: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Link</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.description}</TableTd>
<TableTd>{item.link}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/landing-page/profile/program-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
</Box>
);

View File

@@ -1,225 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import {
Alert,
Box,
Button, Center, FileInput, Group, Image, Paper,
Stack,
Text,
TextInput,
Title
} from '@mantine/core';
import { IconAlertCircle, IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
import { toast } from 'react-toastify';
import ApiFetch from '@/lib/api-fetch';
function EditStrukturPPID() {
const strukturPPID = useProxy(stateStrukturPPID);
const params = useParams();
const router = useRouter()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/ppid/struktur-ppid");
return;
}
try {
const profileData = await strukturPPID.loadForEdit(id);
if (profileData && profileData.image?.link) {
setPreviewImage(profileData.image.link);
}
} catch (error) {
console.error("Error loading profile:", error);
toast.error("Gagal memuat data profile");
}
};
loadData();
return () => {
strukturPPID.editStruktur.reset(); // cleanup form
};
}, [params?.id, router]);
const handleFieldChange = (field: string, value: string) => {
strukturPPID.editStruktur.updateField(field as any, value);
};
const handleFileChange = (newFile: File | null) => {
if (!newFile) {
setFile(null);
return;
}
setFile(newFile);
const reader = new FileReader();
reader.onload = (event) => {
setPreviewImage(event.target?.result as string);
};
reader.readAsDataURL(newFile);
};
const handleSubmit = async () => {
if (isSubmitting || !strukturPPID.editStruktur.form.name.trim()) {
toast.error("Nama wajib diisi");
return;
}
setIsSubmitting(true);
try {
// Upload file jika ada
if (file) {
const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = uploadResponse.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload gambar");
return;
}
strukturPPID.editStruktur.form.imageId = uploaded.id;
}
// Submit form
const success = await strukturPPID.editStruktur.submit();
if (success) {
toast.success("Berhasil menyimpan perubahan");
router.push("/admin/ppid/struktur-ppid");
}
} catch (error) {
console.error("Error submitting form:", error);
toast.error("Gagal menyimpan profile");
} finally {
setIsSubmitting(false);
}
};
const handleBack = () => {
router.back();
};
if (strukturPPID.struktur.loading) {
return (
<Box>
<Center h={400}>
<Text>Memuat data struktur...</Text>
</Center>
</Box>
);
}
// Error state
if (strukturPPID.struktur.error) {
return (
<Box>
<Stack gap="md">
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{strukturPPID.struktur.error}</Text>
</Alert>
</Stack>
</Box>
);
}
// No data state
if (!strukturPPID.struktur.data) {
return (
<Box>
<Stack gap="md">
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
<Alert icon={<IconAlertCircle size={16} />} color="yellow">
<Text fw="bold">Data tidak ditemukan</Text>
<Text>Profile PPID tidak dapat ditemukan</Text>
</Alert>
</Stack>
</Box>
);
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={handleBack}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Struktur PPID</Title>
<TextInput
value={strukturPPID.editStruktur.form.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="Masukkan judul"
/>
{/* File Upload */}
<FileInput
label={<Text fz="sm" fw="bold">Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={handleFileChange}
accept="image/*"
/>
{/* Preview Gambar */}
<Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
{previewImage ? (
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
) : (
<Center w={200} h={200} bg="gray.2">
<Stack align="center" gap="xs">
<IconImageInPicture size={48} color="gray" />
<Text size="sm" c="gray">Tidak ada gambar</Text>
</Stack>
</Center>
)}
</Box>
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
disabled={isSubmitting || strukturPPID.editStruktur.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditStrukturPPID;

View File

@@ -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: "Pegawai",
value: "pegawai",
href: "/admin/ppid/struktur-ppid/pegawai"
},
{
label: "Posisi Organisasi",
value: "posisiorganisasi",
href: "/admin/ppid/struktur-ppid/posisi-organisasi"
},
{
label: "Struktur Organisasi",
value: "strukturorganisasi",
href: "/admin/ppid/struktur-ppid/struktur-organisasi"
}
];
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}>Struktur PPID Desa Darmasaba</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

@@ -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, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateStrukturPPID from '../../_state/ppid/struktur_ppid/struktur_PPID';
import { useShallowEffect } from '@mantine/hooks';
function Page() {
const router = useRouter()
const strukturPPID = useProxy(stateStrukturPPID)
useShallowEffect(() => {
strukturPPID.struktur.load("1")
}, [])
if (!strukturPPID.struktur.data) {
return <Stack>
<Skeleton radius={10} h={800} />
</Stack>
}
const dataArray = Array.isArray(strukturPPID.struktur.data)
? strukturPPID.struktur.data
: [strukturPPID.struktur.data];
return (
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3}>Preview Struktur PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push(`/admin/ppid/struktur-ppid/${strukturPPID.struktur.data?.id}`)} bg={colors['blue-button']}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
{dataArray.map((item) => (
<Paper key={item.id} p={"xl"} bg={colors['BG-trans']}>
<Box>
<Text fw={"bold"} fz={"h4"}>{item.name}</Text>
<Image
w={{ base: "100%", md: "100%" }}
src={item.image?.link}
alt=''
onError={(e) => {
e.currentTarget.src = "/struktur_ppid.png";
}}
/>
</Box>
</Paper>
))}
</Stack>
</Paper>
);
}
export default Page;

View File

@@ -0,0 +1,275 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
Box,
Button,
Group,
Image,
Paper,
Select,
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';
export default function EditPegawaiPPID() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
const [formData, setFormData] = useState({
namaLengkap: stateOrganisasi.edit.form.namaLengkap,
gelarAkademik: stateOrganisasi.edit.form.gelarAkademik,
imageId: stateOrganisasi.edit.form.imageId,
tanggalMasuk: stateOrganisasi.edit.form.tanggalMasuk,
email: stateOrganisasi.edit.form.email,
telepon: stateOrganisasi.edit.form.telepon,
alamat: stateOrganisasi.edit.form.alamat,
posisiId: stateOrganisasi.edit.form.posisiId,
isActive: stateOrganisasi.edit.form.isActive,
});
// Format date to YYYY-MM-DD for date input
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toISOString().split('T')[0];
};
useEffect(() => {
stateStrukturPPID.posisiOrganisasi.findMany.load();
const loadPegawai = async () => {
try {
const data = await stateOrganisasi.edit.load(id);
if (data) {
setFormData({
namaLengkap: data.namaLengkap || "",
gelarAkademik: data.gelarAkademik || "",
imageId: data.imageId || "",
tanggalMasuk: data.tanggalMasuk || "",
email: data.email || "",
telepon: data.telepon || "",
alamat: data.alamat || "",
posisiId: data.posisiId || "",
isActive: data.isActive ?? true, // pakai nullish coalescing
});
if (data.image?.link) {
setPreviewImage(data.image.link);
} else {
setPreviewImage(null);
}
}
} catch (error) {
console.error("Error loading pegawai:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengambil data pegawai"
);
}
};
loadPegawai();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.namaLengkap.trim()) {
toast.error('Nama lengkap tidak boleh kosong');
return;
}
stateOrganisasi.edit.form = {
namaLengkap: formData.namaLengkap.trim(),
gelarAkademik: formData.gelarAkademik.trim(),
imageId: formData.imageId || "",
tanggalMasuk: formData.tanggalMasuk.trim(),
email: formData.email.trim(),
telepon: formData.telepon.trim(),
alamat: formData.alamat.trim(),
posisiId: formData.posisiId.trim(),
isActive: formData.isActive,
};
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
stateOrganisasi.edit.form.imageId = uploaded.id;
}
if (id && !stateOrganisasi.edit.id) {
stateOrganisasi.edit.id = id;
}
const success = await stateOrganisasi.edit.submit();
if (success) {
router.push("/admin/ppid/struktur-ppid/pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pegawai");
}
};
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 Data Pegawai PPID</Title>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
value={formData.namaLengkap}
onChange={(e) => setFormData({ ...formData, namaLengkap: e.target.value })}
/>
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
value={formData.gelarAkademik}
onChange={(e) => setFormData({ ...formData, gelarAkademik: e.target.value })}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
if (file) {
setFile(file);
setPreviewImage(URL.createObjectURL(file)); // Buat preview
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Image
src={previewImage}
alt="Preview"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
/>
)}
</Box>
</Box>
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
value={formatDateForInput(formData.tanggalMasuk)}
onChange={(e) => setFormData({ ...formData, tanggalMasuk: e.target.value })}
/>
<TextInput
label="Email"
placeholder="Contoh: email@example.com"
value={formData.email}
onChange={(e) => (formData.email = e.currentTarget.value)}
/>
<TextInput
label="Telepon"
placeholder="Contoh: 08123456789"
value={formData.telepon}
onChange={(e) => (formData.telepon = e.currentTarget.value)}
/>
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
value={formData.alamat}
onChange={(e) => (formData.alamat = e.currentTarget.value)}
/>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={
stateStrukturPPID.posisiOrganisasi.findMany.data?.map((p) => ({
value: p.id, // harus string
label: p.nama,
})) || []
}
value={formData.posisiId}
onChange={(value) => {
if (value !== null) {
setFormData({ ...formData, posisiId: value }); // value harus string
}
}}
/>
<Select
label="Status Pegawai"
data={[
{ value: 'true', label: 'Aktif' },
{ value: 'false', label: 'Tidak Aktif' },
]}
value={String(formData.isActive)} // 'true' atau 'false'
onChange={(val) => {
setFormData({ ...formData, isActive: val === 'true' });
}}
/>
<Group>
<Button
onClick={handleSubmit}
color="blue"
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box >
);
}

View File

@@ -0,0 +1,148 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailPegawai() {
const statePegawai = useProxy(stateStrukturPPID.pegawai)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter();
useShallowEffect(() => {
statePegawai.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
statePegawai.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ppid/struktur-ppid/pegawai")
}
}
if (!statePegawai.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 Pegawai PPID</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Lengkap</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.namaLengkap}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gelar Akademik</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.gelarAkademik}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Image</Text>
{statePegawai.findUnique.data?.image?.link ? (
<Image src={statePegawai.findUnique.data?.image?.link} alt='' />
) : (
<Text fz={"md"} c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal Masuk</Text>
<Text fz={"lg"}>
{statePegawai.findUnique.data?.tanggalMasuk
? new Date(statePegawai.findUnique.data.tanggalMasuk).toLocaleDateString()
: "-"}
</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Email</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Telepon</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.telepon}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Posisi</Text>
<Stack gap={4}>
{statePegawai.findUnique.data?.posisi ? (
<Text fz={"lg"}>
{statePegawai.findUnique.data.posisi.nama}
</Text>
) : (
<Text fz={"lg"} c="dimmed">
Tidak ada posisi
</Text>
)}
</Stack>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Aktif</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.isActive ? "Ya" : "Tidak"}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
setSelectedId(statePegawai.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={!statePegawai.findUnique.data}
color="red">
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
router.push(`/admin/ppid/struktur-ppid/pegawai/${statePegawai.findUnique.data.id}/edit`);
}
}}
disabled={!statePegawai.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 pegawai ini?"
/>
</Box>
);
}
export default DetailPegawai;

View File

@@ -0,0 +1,200 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, 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';
function CreatePegawaiPPID() {
const router = useRouter();
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
useEffect(() => {
stateStrukturPPID.posisiOrganisasi.findMany.load();
resetForm();
}, []);
const resetForm = () => {
stateOrganisasi.create.form = {
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
};
};
const handleSubmit = async () => {
if (!previewImage) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
try {
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file: previewImage.file,
name: previewImage.file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Set status aktif secara otomatis
stateOrganisasi.create.form.isActive = true;
// Simpan ID gambar ke form
stateOrganisasi.create.form.imageId = uploaded.id;
// Submit form
await stateOrganisasi.create.submit();
// Reset form dan redirect
resetForm();
toast.success("Data pegawai berhasil ditambahkan");
router.push("/admin/ppid/struktur-ppid/pegawai");
} catch (error) {
console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Pegawai</Title>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
value={stateOrganisasi.create.form.namaLengkap}
onChange={(e) => (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
/>
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
value={stateOrganisasi.create.form.gelarAkademik}
onChange={(e) => (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
});
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Image
src={previewImage.preview}
alt="Preview"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
/>
)}
</Box>
</Box>
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
value={stateOrganisasi.create.form.tanggalMasuk}
onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
/>
<TextInput
label="Email"
placeholder="Contoh: email@example.com"
value={stateOrganisasi.create.form.email}
onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)}
/>
<TextInput
label="Telepon"
placeholder="Contoh: 08123456789"
value={stateOrganisasi.create.form.telepon}
onChange={(e) => (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
/>
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
value={stateOrganisasi.create.form.alamat}
onChange={(e) => (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
/>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={stateStrukturPPID.posisiOrganisasi.findMany.data?.map(p => ({
value: p.id,
label: p.nama
})) || []}
value={stateOrganisasi.create.form.posisiId}
onChange={(value) => {
if (value) stateOrganisasi.create.form.posisiId = value;
}}
searchable
/>
<Button
onClick={handleSubmit}
color="blue"
>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreatePegawaiPPID;

View File

@@ -0,0 +1,178 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, ThemeIcon } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
function PegawaiPPID() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pegawai PPID'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPegawaiPPID search={search} />
</Box>
);
}
function ListPegawaiPPID({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useEffect(() => {
load(page, 10);
}, [page]);
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.namaLengkap?.toLowerCase().includes(keyword) ||
item.gelarAkademik?.toLowerCase().includes(keyword) ||
item.telepon?.toLowerCase().includes(keyword) ||
item.posisi?.nama?.toLowerCase().includes(keyword)
);
})
.sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
}, [data, search]);
// Handle loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pegawai'
href='/admin/ppid/struktur-ppid/pegawai/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Gelar Akademik</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Posisi</TableTh>
<TableTh>Aktif</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
</Box>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
<JudulList
title='List Pegawai'
href='/admin/ppid/struktur-ppid/pegawai/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Gelar Akademik</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Posisi</TableTh>
<TableTh>Aktif</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{(() => {
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
return null;
})()}
{([...filteredData]
.sort((a, b) => {
if (a.isActive === b.isActive) {
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
}
return Number(b.isActive) - Number(a.isActive); // aktif duluan
}) // Aktif di atas
).map((item) => (
<TableTr key={item.id}>
<TableTd>{item.namaLengkap}</TableTd>
<TableTd>{item.gelarAkademik}</TableTd>
<TableTd>{item.telepon}</TableTd>
<TableTd>{item.posisi?.nama}</TableTd>
<TableTd>
<Group gap="xs" wrap="nowrap">
<Box visibleFrom="sm">
<Badge color={item.isActive ? "green" : "red"}>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</Box>
<Box hiddenFrom="sm">
{item.isActive ? (
<ThemeIcon color="green" variant="light" size="sm">
<IconCheck size={16} />
</ThemeIcon>
) : (
<ThemeIcon color="red" variant="light" size="sm">
<IconX size={16} />
</ThemeIcon>
)}
</Box>
</Group>
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default PegawaiPPID;

View File

@@ -0,0 +1,117 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
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 EditPosisiOrganisasiPPID() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const stateOrganisasi = useProxy(stateStrukturPPID.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/ppid/struktur-ppid/posisi-organisasi");
}
} catch (error) {
console.error("Error updating posisi organisasi:", 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 Posisi Organisasi PPID</Title>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Posisi Organisasi</Text>}
placeholder='Masukkan nama posisi organisasi'
/>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
}}
/>
<TextInput
value={formData.hierarki}
onChange={(e) => setFormData({ ...formData, hierarki: parseInt(e.target.value) })}
label={<Text fw={"bold"} fz={"sm"}>Hierarki</Text>}
placeholder='Masukkan hierarki'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPosisiOrganisasiPPID;

View File

@@ -0,0 +1,79 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
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 CreatePosisiOrganisasiPPID() {
const router = useRouter();
const stateOrganisasi = useProxy(stateStrukturPPID.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/ppid/struktur-ppid/posisi-organisasi")
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Posisi Organisasi PPID</Title>
<TextInput
label="Nama Posisi"
placeholder="Contoh: Kepala Desa"
value={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
/>
<CreateEditor
value={stateOrganisasi.create.form.deskripsi}
onChange={(htmlContent) => {
stateOrganisasi.create.form.deskripsi = htmlContent;
}}
/>
<TextInput
label="Hierarki"
type="number"
placeholder="Contoh: 1"
value={stateOrganisasi.create.form.hierarki}
onChange={(e) => {
const value = parseInt(e.currentTarget.value, 10);
if (!isNaN(value)) {
stateOrganisasi.create.form.hierarki = value;
}
}}
/>
<Button
onClick={handleSubmit}
color="blue"
>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreatePosisiOrganisasiPPID;

View File

@@ -0,0 +1,134 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
function PosisiOrganisasiPPID() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Posisi Organisasi PPID'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPosisiOrganisasiPPID search={search} />
</Box>
);
}
function ListPosisiOrganisasiPPID({ search }: { search: string }) {
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useEffect(() => {
stateOrganisasi.findMany.load()
}, [])
const handleHapus = async () => {
if (selectedId) {
await stateOrganisasi.delete.byId(selectedId);
setModalHapus(false)
setSelectedId(null)
}
}
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword) ||
item.hierarki?.toString().toLowerCase().includes(keyword)
);
})
.sort((a, b) => a.hierarki - b.hierarki);
if (!stateOrganisasi.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Posisi Organisasi PPID'
href='/admin/ppid/struktur-ppid/posisi-organisasi/create'
/>
<Box style={{ overflowY: 'auto', maxHeight: '400px' }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Posisi</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Hierarki</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody >
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Text truncate dangerouslySetInnerHTML={{ __html: item.deskripsi ?? "" }} />
</TableTd>
<TableTd>{item.hierarki}</TableTd>
<TableTd>
<Button color="green"
onClick={() => {
if (item) {
router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`);
}
}}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red"
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={!item}
>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus posisi organisasi PPID ini?"
/>
</Box>
);
}
export default PosisiOrganisasiPPID;

View File

@@ -0,0 +1,117 @@
/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { OrganizationChart } from 'primereact/organizationchart';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
function StrukturOrganisasiPPID() {
return (
<Box >
<ListStrukturOrganisasiPPID />
</Box>
);
}
function ListStrukturOrganisasiPPID() {
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
useEffect(() => {
stateOrganisasi.findMany.load()
}, [])
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
);
}
// Step 1: Group pegawai berdasarkan posisiId
const posisiMap = new Map<string, any>();
for (const pegawai of stateOrganisasi.findMany.data) {
const posisiId = pegawai.posisi.id;
if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, {
...pegawai.posisi,
pegawaiList: [],
children: []
});
}
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
}
// Step 2: Buat struktur pohon berdasarkan parentId
let root: any[] = [];
posisiMap.forEach((posisi) => {
if (posisi.parentId) {
const parent = posisiMap.get(posisi.parentId);
if (parent) {
parent.children.push(posisi);
}
} else {
root.push(posisi);
}
});
// Step 3: Ubah struktur ke format OrganizationChart
function toOrgChartFormat(node: any): any {
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Tidak ada pegawai',
status: node.nama,
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png'
},
children: node.children.map(toOrgChartFormat)
};
}
const chartData = root.map(toOrgChartFormat);
return (
<Box py={10}>
<Paper bg={colors.grey} p="md" style={{overflowX: 'auto'}}>
<OrganizationChart value={chartData} nodeTemplate={nodeTemplate} />
</Paper>
</Box>
);
}
function nodeTemplate(node: any) {
const imageSrc = node?.data?.image || '/img/default.png';
const name = node?.data?.name || 'Tanpa Nama';
const status = node?.data?.status || 'Tidak ada deskripsi';
return (
<Stack align="center" gap={4}>
<Image
src={imageSrc}
alt={name}
radius="xl"
w={120}
h={120}
fit="cover"
/>
<Text fw={600} ta="center">{name}</Text>
<Text size="sm" c="dimmed" ta="center">{status}</Text>
</Stack>
);
}
export default StrukturOrganisasiPPID;

View File

@@ -49,7 +49,7 @@ export const navBar = [
{
id: "PPID_2",
name: "Struktur PPID",
path: "/admin/ppid/struktur-ppid"
path: "/admin/ppid/struktur-ppid/pegawai"
},
{
id: "PPID_3",