amalia/07-jan-26 #102

Merged
amaliadwiy merged 3 commits from amalia/07-jan-26 into main 2026-01-07 17:11:51 +08:00
4 changed files with 330 additions and 18 deletions

View File

@@ -52,7 +52,10 @@ type FormSurat = {
syaratDokumen: DataItem[];
};
type ErrorState = Record<string, string | null>;
export default function FormSurat() {
const [errors, setErrors] = useState<ErrorState>({});
const [opened, { open, close }] = useDisclosure(false);
const [noPengajuan, setNoPengajuan] = useState("");
const [submitLoading, setSubmitLoading] = useState(false);
@@ -150,22 +153,24 @@ export default function FormSurat() {
}, [jenisSuratFix.id]);
function onChecking() {
const hasError = Object.values(errors).some((v) => v);
if (hasError) {
return notification({
title: "Gagal",
message: "Masih ada form yang belum valid",
type: "error",
});
}
const isFormKosong = Object.values(formSurat).some((value) => {
if (Array.isArray(value)) {
return (
value.length === 0 ||
value.some(
(item) =>
typeof item.value === "string" && item.value.trim() === "",
)
return value.some(
(item) =>
typeof item.value === "string" && item.value.trim() === "",
);
}
if (typeof value === "string") {
return value.trim() === "";
}
return false;
return typeof value === "string" && value.trim() === "";
});
if (isFormKosong) {
@@ -174,9 +179,9 @@ export default function FormSurat() {
message: "Silahkan lengkapi form surat",
type: "error",
});
} else {
open();
}
open();
}
async function onSubmit() {
@@ -239,6 +244,30 @@ export default function FormSurat() {
);
}
function validateField(key: string, value: any) {
const stringValue = String(value ?? "").trim();
// wajib diisi
if (!stringValue) {
return "Field wajib diisi";
}
// 🔥 semua key yang mengandung "nik"
if (key.toLowerCase().includes("nik")) {
if (!/^\d+$/.test(stringValue)) {
return "NIK harus berupa angka";
}
if (stringValue.length !== 16) {
return "NIK harus 16 digit";
}
}
return null;
}
function validationForm({
key,
value,
@@ -246,12 +275,27 @@ export default function FormSurat() {
key: "nama" | "phone" | "dataPelengkap" | "syaratDokumen";
value: any;
}) {
if (key == "dataPelengkap" || key == "syaratDokumen") {
if (key === "dataPelengkap" || key === "syaratDokumen") {
const errorMsg = validateField(value.key, value.value);
setErrors((prev) => ({
...prev,
[value.key]: errorMsg,
}));
setFormSurat((prev) => ({
...prev,
[key]: updateArrayByKey(prev[key], value.key, value.value),
}));
} else {
const keyFix = key == "nama" ? "nama_kontak" : key;
const errorMsg = validateField(keyFix, value);
setErrors((prev) => ({
...prev,
[keyFix]: errorMsg,
}));
setFormSurat({
...formSurat,
[key]: value,
@@ -259,6 +303,7 @@ export default function FormSurat() {
}
}
return (
<Container size="md" w={"100%"} pb={"lg"}>
<Modal
@@ -358,6 +403,7 @@ export default function FormSurat() {
label={<FieldLabel label="Nama" hint="Nama kontak" />}
placeholder="Budi Setiawan"
value={formSurat.nama}
error={errors.nama_kontak}
onChange={(e) =>
validationForm({ key: "nama", value: e.target.value })
}
@@ -374,6 +420,8 @@ export default function FormSurat() {
}
placeholder="08123456789"
value={formSurat.phone}
error={errors.phone}
type="number"
onChange={(e) =>
validationForm({ key: "phone", value: e.target.value })
}
@@ -446,6 +494,7 @@ export default function FormSurat() {
/>
) : (
<TextInput
error={errors[item.key]}
type={item.type}
label={
<FieldLabel

View File

@@ -380,6 +380,7 @@ function DataUpdate({
}) {
const [opened, { open, close }] = useDisclosure(false);
const navigate = useNavigate();
const [errors, setErrors] = useState<Record<string, string | null>>({});
const [sukses, setSukses] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [dataPelengkap, setDataPelengkap] = useState<DataItem[]>([]);
@@ -427,6 +428,29 @@ function DataUpdate({
fetchData();
}, []);
function validateField(key: string, value: any) {
const stringValue = String(value ?? "").trim();
// wajib diisi
if (!stringValue) {
return "Field wajib diisi";
}
// 🔥 semua key yg mengandung "nik"
if (key.toLowerCase().includes("nik")) {
if (!/^\d+$/.test(stringValue)) {
return "NIK harus berupa angka";
}
if (stringValue.length !== 16) {
return "NIK harus 16 digit";
}
}
return null;
}
function upsertById<T extends { id: string }>(array: T[], item: T): T[] {
const index = array.findIndex((v) => v.id === item.id);
@@ -446,6 +470,13 @@ function DataUpdate({
kategori: "dataPelengkap" | "syaratDokumen";
value: UpdateDataItem;
}) {
const errorMsg = validateField(value.key, value.value);
setErrors((prev) => ({
...prev,
[value.id]: errorMsg,
}));
setFormSurat((prev) => ({
...prev,
[kategori]: upsertById(prev[kategori], {
@@ -456,6 +487,7 @@ function DataUpdate({
}));
}
function updateArrayByKey(
list: UpdateDataItem[],
id: string,
@@ -465,6 +497,16 @@ function DataUpdate({
}
function onChecking() {
const hasError = Object.values(errors).some((v) => v);
if (hasError) {
return notification({
title: "Gagal",
message: "Masih ada data yang belum valid",
type: "error",
});
}
if (
formSurat.dataPelengkap.length == 0 &&
formSurat.syaratDokumen.length == 0
@@ -670,13 +712,15 @@ function DataUpdate({
(n: any) => n.key === item.key,
)?.value,
)
: parseTanggalID(item.value)
: parseTanggalID(item.value)
}
/>
) : (
<TextInput
error={errors[item.id]}
label={<FieldLabel label={item.name} hint={item.desc} />}
placeholder={item.name}
type={item.type}
onChange={(e) =>
validationForm({
kategori: "dataPelengkap",

View File

@@ -2,7 +2,9 @@ import ModalFile from "@/components/ModalFile";
import ModalSurat from "@/components/ModalSurat";
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import { parseTanggalID } from "@/server/lib/stringToDate";
import {
ActionIcon,
Anchor,
Badge,
Button,
@@ -14,24 +16,31 @@ import {
Group,
List,
Modal,
Select,
Spoiler,
Stack,
Table,
Text,
Textarea,
TextInput,
ThemeIcon,
Title,
Tooltip
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconCheck,
IconEdit,
IconFileCertificate,
IconFileCheck,
IconInfoCircle,
IconMessageReport,
IconPhone,
IconUser,
} from "@tabler/icons-react";
import dayjs from "dayjs";
import type { User } from "generated/prisma";
import type { JsonValue } from "generated/prisma/runtime/library";
import _ from "lodash";
@@ -93,6 +102,7 @@ function DetailDataPengajuan({
dataText: any;
onAction: () => void;
}) {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [keterangan, setKeterangan] = useState("");
@@ -103,6 +113,9 @@ function DetailDataPengajuan({
const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [viewImg, setViewImg] = useState({ file: "", folder: "" });
const [uploading, setUploading] = useState({ ok: false, file: "" });
const [editValue, setEditValue] = useState({ id: "", jenis: "", val: "", option: null as any, type: "", key: "" })
const [openEdit, setOpenEdit] = useState(false)
const [loadingUpdate, setLoadingUpdate] = useState(false)
useEffect(() => {
async function fetchHost() {
@@ -215,6 +228,43 @@ function DetailDataPengajuan({
}
};
async function updateDataText() {
try {
setLoadingUpdate(true)
const res = await apiFetch.api.pelayanan["update-data-pelengkap"].post({
id: editValue.id,
value: editValue.val,
jenis: editValue.key,
idUser: host?.id ?? "",
})
if (res?.status === 200) {
notification({
title: "Success",
message: "Success update data",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to update data",
type: "error",
})
}
} catch (error) {
console.error(error)
notification({
title: "Error",
message: "Failed to update data",
type: "error",
})
} finally {
setLoadingUpdate(false)
setOpenEdit(false)
onAction()
}
}
useShallowEffect(() => {
if (viewImg) {
setOpenedPreviewFile(true);
@@ -231,9 +281,86 @@ function DetailDataPengajuan({
}
}, [uploading]);
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
return (
<Group justify="apart" gap="xs" align="center">
<Text fw={600}>{label}</Text>
{hint && (
<Tooltip label={hint} withArrow>
<ActionIcon size={24} variant="subtle">
<IconInfoCircle size={16} />
</ActionIcon>
</Tooltip>
)}
</Group>
);
}
return (
<>
{/* MODAL EDIT DATA PELENGKAP */}
<Modal
opened={openEdit}
onClose={() => setOpenEdit(false)}
title={"Edit"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
{editValue.type == "enum" ? (
<Select
allowDeselect={false}
label={<FieldLabel label={editValue.jenis} />}
data={editValue.option ?? []}
placeholder={editValue.jenis}
onChange={(e) => { setEditValue({ ...editValue, val: e ?? "" }) }}
value={editValue.val}
/>
) : editValue.type == "date" ? (
<DateInput
locale="id"
valueFormat="DD MMMM YYYY"
label={<FieldLabel label={editValue.jenis} />}
placeholder={editValue.jenis}
onChange={(e) => {
const formatted = e
? dayjs(e).locale("id").format("DD MMMM YYYY")
: "";
setEditValue({
...editValue,
val: formatted
})
}}
value={
editValue.val
? parseTanggalID(editValue.val)
: parseTanggalID(editValue.val)
}
/>
) : (
<TextInput
label={<FieldLabel label={editValue.jenis} />}
placeholder={editValue.jenis}
type={editValue.type}
onChange={(e) => { setEditValue({ ...editValue, val: e.target.value }) }}
value={editValue.val}
/>
)}
<Group justify="center" grow>
<Button variant="light" onClick={() => { setOpenEdit(false) }}>
Batal
</Button>
<Button
variant="filled"
onClick={updateDataText}
disabled={loadingUpdate || !editValue.val}
loading={loadingUpdate}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
<ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg.file)}
onClose={() => {
@@ -413,7 +540,25 @@ function DetailDataPengajuan({
</Table.Td>
<Table.Td>:</Table.Td>
<Table.Td style={{ width: "85%" }}>
{_.upperFirst(item.value)}
<Flex
gap="md"
justify="flex-start"
align="center"
direction="row"
>
<Text>
{_.upperFirst(item.value)}
</Text>
<ActionIcon
variant="subtle"
aria-label="Edit"
onClick={() => {
setEditValue({ id: item.id, val: item.value, type: item.type, option: item.options, jenis: item.jenis, key: item.key })
setOpenEdit(true)
}}>
<IconEdit size={16} />
</ActionIcon>
</Flex>
</Table.Td>
</Table.Tr>
))}

View File

@@ -308,7 +308,11 @@ const PelayananRoute = new Elysia({
})
const dataTextCategory = (data?.CategoryPelayanan?.dataPelengkap ?? []) as {
name: string;
type: string;
options?: {
label: string,
value: string
}[]; name: string;
desc: string;
key: string;
}[];
@@ -327,7 +331,10 @@ const PelayananRoute = new Elysia({
return {
id: item.id,
jenis: nama,
key: ref?.key,
value: item.value,
type: ref?.type ?? "",
options: ref?.options ?? [],
order: ref?.order ?? Infinity,
};
})
@@ -1050,6 +1057,73 @@ const PelayananRoute = new Elysia({
description: `tool untuk update data pengajuan pelayanan surat`,
}
})
.post("/update-data-pelengkap", async ({ body }) => {
const { id, value, jenis, idUser } = body
const dataPelengkap = await prisma.dataTextPelayanan.findUnique({
where: {
id
},
select: {
idPengajuanLayanan: true,
PelayananAjuan: {
select: {
status: true
}
}
}
})
if (!dataPelengkap) {
return { success: false, message: 'data pelengkap surat tidak ditemukan' }
}
const upd = await prisma.dataTextPelayanan.update({
where: {
id
},
data: {
value: value,
}
})
const history = await prisma.historyPelayanan.create({
data: {
idPengajuanLayanan: dataPelengkap.idPengajuanLayanan,
deskripsi: `Pengajuan surat diupdate oleh user (data yg diupdate: ${jenis})`,
status: dataPelengkap.PelayananAjuan.status,
idUser
}
})
return { success: true, message: 'data pelengkap surat sudah diperbarui' }
}, {
body: t.Object({
id: t.String({
error: "id harus diisi",
description: "ID yang ingin diupdate"
}),
value: t.String({
error: "value harus diisi",
description: "Value yang ingin diupdate"
}),
jenis: t.String({
error: "jenis harus diisi",
description: "Jenis data yang ingin diupdate"
}),
idUser: t.String({
error: "idUser harus diisi",
description: "ID user yang melakukan update"
})
}),
detail: {
summary: "Update Data Pelengkap Pengajuan Pelayanan Surat oleh user admin",
description: `tool untuk update data pelengkap pengajuan pelayanan surat oleh user admin`,
}
})
.get("/list", async ({ query }) => {
const { take, page, search, status } = query
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))