Compare commits

...

12 Commits

Author SHA1 Message Date
5807e98069 fix: tambah pengajuan surat pengantar skck
Deskripsi:
- link nya

No Issues
2026-01-15 14:32:28 +08:00
59b4f1d73f fix: surat keterangan kelahiran
Deskripsi:
- update value tanggal_lahir_ayah

No Issues
2026-01-15 14:19:05 +08:00
8bd552ac22 fix: pelayanan surat
Deskripsi:
- link surat

NO Issues
2026-01-15 14:09:32 +08:00
3da163ea1d fix : error surat 2026-01-15 11:53:10 +08:00
57e4f34eb6 Merge pull request 'amalia/14-jan-26' (#110) from amalia/14-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/110
2026-01-14 17:43:11 +08:00
3348cbe8e3 fix: pelayanan surat
Deskripsi:
- pelayanan surat

No Issues
2026-01-14 17:41:27 +08:00
727984a076 fix: update data pengajuan surat
Deskripsi:
- loading saat melakukan pencarian
- disable select dan input date saat status selesai

No Issues
2026-01-14 15:58:51 +08:00
a7a0ad7e37 Merge pull request 'amalia/13-jan-26' (#109) from amalia/13-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/109
2026-01-13 17:18:01 +08:00
9fed41cbe8 fix: testing surat 2026-01-13 17:10:59 +08:00
fc387fe8e6 fix: menu setting
deskiripsi:
- navigate
- list length

No Issues
2026-01-13 15:38:29 +08:00
80df579499 qc: nomer 2 dan 4
Deskripsi:
- button back
- breadcrumb
- active menu

No Issues
2026-01-13 15:10:08 +08:00
5bbbc15c27 Merge pull request 'fix: qc' (#108) from amalia/12-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/108
2026-01-12 17:47:04 +08:00
9 changed files with 163 additions and 94 deletions

View File

@@ -207,7 +207,7 @@ export default function DesaSetting({
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list?.map((v: any) => (
{list.length > 0 && list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>

View File

@@ -4,19 +4,17 @@ import {
Button,
Divider,
Flex,
Grid,
Group,
Input,
List,
Modal,
Stack,
Table,
Text,
Title,
Tooltip,
Tooltip
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react";
import { IconEye, IconTrash } from "@tabler/icons-react";
import type { JsonValue } from "generated/prisma/runtime/library";
import { useState } from "react";
import useSWR from "swr";
@@ -625,7 +623,7 @@ export default function KategoriPelayananSurat({
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list?.map((v: any) => (
{list.length > 0 && list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>

View File

@@ -1,5 +1,5 @@
import apiFetch from "@/lib/apiFetch";
import { Flex, Loader, Modal, Text } from "@mantine/core";
import { Flex, Modal, Progress, Stack, Text } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
@@ -23,7 +23,7 @@ export default function ModalSurat({
surat,
}: {
open: boolean;
onClose: (val: any) => void;
onClose: (val: { success: boolean, data: string }) => void;
surat: string;
}) {
const A4Style = {
@@ -35,7 +35,7 @@ export default function ModalSurat({
fontSize: "14px",
fontFamily: "Times New Roman",
};
const [uploading, setUploading] = useState<"Menyiapkan" | "Mengupload" | "Selesai">("Menyiapkan")
const [uploading, setUploading] = useState<{ text: "Menyiapkan" | "Mengupload" | "Selesai" | "Gagal", value: number }>({ text: "Menyiapkan", value: 10 })
const hiddenRef = useRef<any>(null);
const { data, mutate, isLoading } = useSWR("surat", () =>
apiFetch.api.surat.detail.get({
@@ -45,6 +45,7 @@ export default function ModalSurat({
}),
);
useShallowEffect(() => {
mutate();
}, []);
@@ -52,7 +53,7 @@ export default function ModalSurat({
const uploadPdf = async () => {
try {
if (data && data.data && data.data.surat && (data.data.surat.file == "" || data.data.surat.file == null)) {
setUploading("Mengupload");
setUploading({ text: "Mengupload", value: 75 });
const element = hiddenRef.current;
const canvas = await html2canvas(element, {
scale: 2,
@@ -95,9 +96,21 @@ export default function ModalSurat({
filename: resImg.data?.filename!,
});
setUploading("Selesai");
if (resUpdate?.data?.success) {
setUploading({ text: "Selesai", value: 100 });
setTimeout(() => {
onClose({ success: true, data: resUpdate.data?.link });
}, 1000)
} else {
setUploading({ text: "Gagal", value: 100 });
setTimeout(() => {
onClose({ success: false, data: "" });
}, 1000)
}
} else {
setUploading({ text: "Gagal", value: 100 });
setTimeout(() => {
onClose(resUpdate.data?.link);
onClose({ success: false, data: "" });
}, 1000)
}
@@ -110,7 +123,7 @@ export default function ModalSurat({
if (open) {
setTimeout(() => {
uploadPdf();
}, 5000);
}, 3000);
}
}, [surat, open]);
@@ -136,14 +149,24 @@ export default function ModalSurat({
},
}}
title={
<Flex justify="space-between" align="center" w="100%">
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
<>
<Flex justify="space-between" align="center" w="100%">
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
<Flex gap={8} align="center">
{/* <Flex gap={8} align="center">
<Loader color="blue" size="xs" />
<Text size="sm">{uploading}</Text>
<Text size="sm">{uploading.text}</Text>
</Flex> */}
</Flex>
</Flex>
<Stack
align="stretch"
justify="center"
gap="xs"
>
<Text size="sm" ta="center">{uploading.text} - Harap menunggu sampai selesai</Text>
<Progress radius="md" value={uploading.value} animated size="lg" />
</Stack>
</>
}
>
<div ref={hiddenRef} style={A4Style}>

View File

@@ -7,7 +7,7 @@ export default function SKKelahiran({ data }: { data: any }) {
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"",
"",
);
const loadImage = async () => {
@@ -161,7 +161,7 @@ export default function SKKelahiran({ data }: { data: any }) {
<tr>
<td>Tempat & Tanggal Lahir</td>
<td>:</td>
<td>{`${getValue("tempat_lahir_ayah")}, ${"tanggal_lahir_ayah"}`}</td>
<td>{`${getValue("tempat_lahir_ayah")}, ${getValue("tanggal_lahir_ayah")}`}</td>
</tr>
<tr>
<td>Pekerjaan</td>

View File

@@ -96,7 +96,7 @@ export default function FormSurat() {
setJenisSuratFix({ name: "", id: "" });
} else {
const namaJenis = fromSlug(jenisSurat);
const data = listCategory.find((item: any) => item.name == namaJenis);
const data = listCategory.find((item: any) => item.name.toUpperCase() == namaJenis.toUpperCase());
if (!data) return;
setJenisSuratFix(data);
}

View File

@@ -317,57 +317,60 @@ function SearchData() {
}
return (
<FormSection
title="Cari Pengajuan Surat"
info="Masukkan nomor pengajuan dan nomor telepon yang digunakan saat pengajuan surat."
>
<Grid>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nomor Pengajuan"
hint="Nomor pengajuan surat"
/>
}
placeholder="PS-2025-000123"
onChange={(e) => {
setSearchPengajuan(e.target.value);
}}
/>
</Grid.Col>
<>
<FullScreenLoading visible={submitLoading} text="Mencari Data" />
<FormSection
title="Cari Pengajuan Surat"
info="Masukkan nomor pengajuan dan nomor telepon yang digunakan saat pengajuan surat."
>
<Grid>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nomor Pengajuan"
hint="Nomor pengajuan surat"
/>
}
placeholder="PS-2025-000123"
onChange={(e) => {
setSearchPengajuan(e.target.value);
}}
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nomor Telephone"
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
/>
}
placeholder="08123456789"
type="number"
onChange={(e) => {
setSearchPengajuanPhone(e.target.value);
}}
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nomor Telephone"
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
/>
}
placeholder="08123456789"
type="number"
onChange={(e) => {
setSearchPengajuanPhone(e.target.value);
}}
/>
</Grid.Col>
<Grid.Col span={12}>
<Button
fullWidth
variant="light"
color="blue"
onClick={() => {
handleSearch();
}}
loading={submitLoading}
>
Cari Pengajuan
</Button>
</Grid.Col>
</Grid>
</FormSection>
<Grid.Col span={12}>
<Button
fullWidth
variant="light"
color="blue"
onClick={() => {
handleSearch();
}}
loading={submitLoading}
>
Cari Pengajuan
</Button>
</Grid.Col>
</Grid>
</FormSection>
</>
);
}
@@ -387,6 +390,7 @@ function DataUpdate({
const [dataSyaratDokumen, setDataSyaratDokumen] = useState<DataItem[]>([]);
const [dataPengajuan, setDataPengajuan] = useState<DataPengajuan | {}>({});
const [status, setStatus] = useState("");
const [loadingFetchData, setLoadingFetchData] = useState(false);
const [formSurat, setFormSurat] = useState<FormUpdateSurat>({
dataPelengkap: [],
syaratDokumen: [],
@@ -394,6 +398,7 @@ function DataUpdate({
async function fetchData() {
try {
setLoadingFetchData(true);
const res = await apiFetch.api.pelayanan["detail-data"].post({
nomerPengajuan: noPengajuan,
});
@@ -421,6 +426,8 @@ function DataUpdate({
}
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoadingFetchData(false);
}
}
@@ -600,7 +607,7 @@ function DataUpdate({
return (
<>
<FullScreenLoading visible={submitLoading} />
<FullScreenLoading visible={submitLoading || loadingFetchData} />
<Modal
opened={opened}
onClose={close}
@@ -667,6 +674,7 @@ function DataUpdate({
<Grid.Col span={6} key={index}>
{item.type == "enum" ? (
<Select
disabled={status != "ditolak" && status != "antrian"}
allowDeselect={false}
label={<FieldLabel label={item.name} hint={item.desc} />}
data={item.options ?? []}
@@ -686,6 +694,7 @@ function DataUpdate({
/>
) : item.type == "date" ? (
<DateInput
disabled={status != "ditolak" && status != "antrian"}
locale="id"
valueFormat="DD MMMM YYYY"
label={<FieldLabel label={item.name} hint={item.desc} />}
@@ -712,7 +721,7 @@ function DataUpdate({
(n: any) => n.key === item.key,
)?.value,
)
: parseTanggalID(item.value)
: parseTanggalID(item.value)
}
/>
) : (

View File

@@ -73,7 +73,7 @@ export default function DashboardLayout() {
<AppShell
padding="lg"
navbar={{
width: 260,
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: !opened },
}}
@@ -215,8 +215,6 @@ function NavigationDashboard() {
const location = useLocation();
const [permissions, setPermissions] = useState<JsonValue[]>([]);
console.log(location)
useEffect(() => {
async function fetchPermissions() {
const { data } = await apiFetch.api.user.find.get();
@@ -292,14 +290,19 @@ function NavigationDashboard() {
.map((item) => (
<NavLink
key={item.path}
active={isActive(item.path as keyof typeof clientRoute)}
active={isActive(item.path as keyof typeof clientRoute) ||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")}
leftSection={item.icon}
label={
<Flex align="center" gap={6}>
<Text fw={500}>{item.label}</Text>
{(isActive(item.path as keyof typeof clientRoute) ||
{(
isActive(item.path as keyof typeof clientRoute) ||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list")
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
)
&& (
<Badge
@@ -319,7 +322,10 @@ function NavigationDashboard() {
navigate(clientRoutes[item.path as keyof typeof clientRoute])
}
style={{
backgroundColor: isActive(item.path as keyof typeof clientRoute)
backgroundColor: isActive(item.path as keyof typeof clientRoute) ||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
? "rgba(0,255,200,0.1)"
: "transparent",
borderRadius: "8px",

View File

@@ -1,4 +1,5 @@
import BreadCrumbs from "@/components/BreadCrumbs";
import FullScreenLoading from "@/components/FullScreenLoading";
import ModalFile from "@/components/ModalFile";
import ModalSurat from "@/components/ModalSurat";
import notification from "@/components/notificationGlobal";
@@ -125,6 +126,7 @@ function DetailDataPengajuan({
const [editValue, setEditValue] = useState({ id: "", jenis: "", val: "", option: null as any, type: "", key: "" })
const [openEdit, setOpenEdit] = useState(false)
const [loadingUpdate, setLoadingUpdate] = useState(false)
const [loadingFS, setLoadingFS] = useState({ value: false, text: "" })
useEffect(() => {
async function fetchHost() {
@@ -143,6 +145,7 @@ function DetailDataPengajuan({
async function sendWA({ status, linkSurat, linkUpdate }: { status: string, linkSurat: string, linkUpdate: string }) {
try {
setLoadingFS({ value: true, text: "Sending message to warga" })
const resWA = await apiFetch.api["send-wa"]["pengajuan-surat"].post({
noPengajuan: data?.noPengajuan ?? "",
jenisSurat: data?.category ?? "",
@@ -160,6 +163,11 @@ function DetailDataPengajuan({
message: "Success send message to warga",
type: "success",
});
if (status == "selesai") {
onAction()
}
} else {
notification({
title: "Failed",
@@ -181,10 +189,14 @@ function DetailDataPengajuan({
type: "error",
});
}
finally {
setLoadingFS({ value: false, text: "" })
}
}
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try {
setLoadingFS({ value: true, text: "Updating status" })
const statusFix = cat == "tolak"
? "ditolak"
: data.status == "antrian"
@@ -234,6 +246,8 @@ function DetailDataPengajuan({
message: "Failed to update pengajuan surat",
type: "error",
});
} finally {
setLoadingFS({ value: false, text: "" })
}
};
@@ -307,6 +321,8 @@ function DetailDataPengajuan({
return (
<>
<FullScreenLoading visible={loadingFS.value} text={loadingFS.text} />
{/* MODAL EDIT DATA PELENGKAP */}
<Modal
opened={openEdit}
@@ -374,6 +390,7 @@ function DetailDataPengajuan({
open={openedPreviewFile && !_.isEmpty(viewImg.file)}
onClose={() => {
setOpenedPreviewFile(false);
setViewImg({ file: "", folder: "" })
}}
folder={viewImg.folder}
fileName={viewImg.file}
@@ -448,12 +465,14 @@ function DetailDataPengajuan({
)}
</Stack>
</Modal>
{/* MODAL PREVIEW SURAT */}
{data?.status == "selesai" && !data?.fileSurat && (
<ModalSurat
open={openedPreview}
onClose={(val) => {
setOpenedPreview(false)
setUploading({ ok: true, file: val })
setUploading({ ok: val.success, file: val.data })
}}
surat={data?.idSurat}
/>
@@ -625,18 +644,31 @@ function DetailDataPengajuan({
Setujui
</Button>
</Group>
) : data?.status === "selesai" ? (
<Group justify="center" grow>
<Button
variant="light"
onClick={() => { setViewImg({ file: data?.fileSurat, folder: "surat" }) }}
>
Surat
</Button>
</Group>
) : (
<></>
)}
) : data?.status === "selesai" ?
!data?.fileSurat ?
(
<Group justify="center" grow>
<Button
variant="light"
onClick={() => { setOpenedPreview(true) }}
>
Kirim Ulang Surat
</Button>
</Group>
)
:
(
<Group justify="center" grow>
<Button
variant="light"
onClick={() => { setViewImg({ file: data?.fileSurat, folder: "surat" }) }}
>
Surat
</Button>
</Group>
) : (
<></>
)}
</Grid.Col>
</Grid>
</Stack>

View File

@@ -18,12 +18,13 @@ import {
import type { JsonValue } from "generated/prisma/runtime/library";
import _ from "lodash";
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
export default function DetailSettingPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const type = query.get("type");
const navigate = useNavigate();
const [permissions, setPermissions] = useState<JsonValue[]>([]);
const dataMenu = [
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
@@ -114,7 +115,7 @@ export default function DetailSettingPage() {
.map((item) => (
<NavLink
key={item.key}
href={"?type=" + item.path}
onClick={()=>{navigate("?type=" + item.path)}}
label={item.label}
leftSection={item.icon}
active={