fix: Quality Control improvements & bug fixes
- APBDes: Fix edit form original data tracking (imageId, fileId) - APBDes: Update formula consistency in state - PPID modules: Various UI improvements and bug fixes - PPID Profil: Preview and edit page improvements - PPID Dasar Hukum: Page structure improvements - PPID Visi Misi: Page structure improvements - PPID Struktur: Posisi organisasi page improvements - PPID Daftar Informasi: Edit page improvements - Auth login: Route improvements - Update dependencies (package.json, bun.lockb) - Update seed data - Update .gitignore QC Reports added: - QC-APBDES-MODULE.md - QC-PROFIL-MODULE.md - QC-SDGS-DESA.md - QC-DESA-ANTI-KORUPSI.md - QC-PRESTASI-DESA-MODULE.md - QC-PPID-PROFIL-MODULE.md - QC-STRUKTUR-PPID-MODULE.md - QC-VISI-MISI-PPID-MODULE.md - QC-DASAR-HUKUM-PPID-MODULE.md - QC-PERMOHONAN-INFORMASI-PUBLIK-MODULE.md - QC-PERMOHONAN-KEBERATAN-INFORMASI-MODULE.md - QC-DAFTAR-INFORMASI-PUBLIK-MODULE.md - QC-IKM-MODULE.md Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,6 +31,9 @@ yarn-error.log*
|
||||
# env
|
||||
.env*
|
||||
|
||||
# QC
|
||||
QC
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"colors": "^1.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.3.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"elysia": "^1.3.5",
|
||||
"embla-carousel": "^8.6.0",
|
||||
|
||||
@@ -69,8 +69,8 @@ import { seedProfilPpd } from "./_seeder_list/ppid/profil-ppid/seed_profil_ppd";
|
||||
|
||||
(async () => {
|
||||
// Always run seedAssets to handle new images without duplication
|
||||
console.log("📂 Checking for new assets to seed...");
|
||||
await seedAssets();
|
||||
// console.log("📂 Checking for new assets to seed...");
|
||||
// await seedAssets();
|
||||
|
||||
// // =========== FILE STORAGE ===========
|
||||
|
||||
|
||||
@@ -38,11 +38,9 @@ function normalizeItem(item: Partial<z.infer<typeof ApbdesItemSchema>>): z.infer
|
||||
const anggaran = item.anggaran ?? 0;
|
||||
const realisasi = item.realisasi ?? 0;
|
||||
|
||||
|
||||
|
||||
|
||||
// ✅ Formula yang benar
|
||||
const selisih = anggaran - realisasi; // positif = sisa anggaran, negatif = over budget
|
||||
const selisih = realisasi - anggaran; // positif = sisa anggaran, negatif = over budget
|
||||
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; // persentase realisasi terhadap anggaran
|
||||
|
||||
return {
|
||||
|
||||
@@ -53,7 +53,7 @@ function EditAPBDes() {
|
||||
const params = useParams();
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
|
||||
// Check if form is valid
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
@@ -76,33 +76,62 @@ function EditAPBDes() {
|
||||
tipe: 'pendapatan',
|
||||
});
|
||||
|
||||
// Type for the API response
|
||||
interface APBDesResponse {
|
||||
id: string;
|
||||
image?: {
|
||||
link: string;
|
||||
id: string;
|
||||
};
|
||||
file?: {
|
||||
link: string;
|
||||
id: string;
|
||||
};
|
||||
// Add other properties as needed
|
||||
}
|
||||
// Simpan data original untuk reset form
|
||||
const [originalData, setOriginalData] = useState({
|
||||
tahun: 0,
|
||||
imageId: '',
|
||||
fileId: '',
|
||||
imageUrl: '',
|
||||
fileUrl: '',
|
||||
});
|
||||
|
||||
// Load data saat pertama kali
|
||||
useEffect(() => {
|
||||
const id = params?.id as string;
|
||||
if (id) {
|
||||
apbdesState.edit.load(id).then((response) => {
|
||||
const data = response as unknown as APBDesResponse;
|
||||
if (data) {
|
||||
// ✅ Ambil link langsung dari response
|
||||
setPreviewImage(data.image?.link || null);
|
||||
setPreviewDoc(data.file?.link || null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!id) return;
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const data = await apbdesState.edit.load(id);
|
||||
|
||||
if (!data) return;
|
||||
|
||||
// Set preview dari data lama
|
||||
setPreviewImage(data.image?.link || null);
|
||||
setPreviewDoc(data.file?.link || null);
|
||||
|
||||
// Simpan data original untuk reset
|
||||
setOriginalData({
|
||||
tahun: data.tahun || new Date().getFullYear(),
|
||||
imageId: data.imageId || '',
|
||||
fileId: data.fileId || '',
|
||||
imageUrl: data.image?.link || '',
|
||||
fileUrl: data.file?.link || '',
|
||||
});
|
||||
|
||||
// Set form dengan data lama (termasuk imageId dan fileId)
|
||||
apbdesState.edit.form = {
|
||||
tahun: data.tahun || new Date().getFullYear(),
|
||||
imageId: data.imageId || '',
|
||||
fileId: data.fileId || '',
|
||||
items: (data.items || []).map((item: any) => ({
|
||||
kode: item.kode,
|
||||
uraian: item.uraian,
|
||||
anggaran: item.anggaran,
|
||||
realisasi: item.realisasi,
|
||||
selisih: item.selisih,
|
||||
persentase: item.persentase,
|
||||
level: item.level,
|
||||
tipe: item.tipe || 'pendapatan',
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading APBDes:', error);
|
||||
toast.error('Gagal memuat data APBDes');
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [params?.id]);
|
||||
|
||||
const handleDrop = (fileType: 'image' | 'doc') => (files: File[]) => {
|
||||
@@ -162,23 +191,38 @@ function EditAPBDes() {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Upload file baru jika ada
|
||||
// Upload file baru jika ada perubahan
|
||||
if (imageFile) {
|
||||
// Hapus file lama dari form jika ada file baru
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file: imageFile,
|
||||
name: imageFile.name,
|
||||
});
|
||||
const imageId = res.data?.data?.id;
|
||||
if (imageId) apbdesState.edit.form.imageId = imageId;
|
||||
if (imageId) {
|
||||
apbdesState.edit.form.imageId = imageId;
|
||||
}
|
||||
}
|
||||
|
||||
if (docFile) {
|
||||
// Hapus file lama dari form jika ada file baru
|
||||
const res = await ApiFetch.api.fileStorage.create.post({
|
||||
file: docFile,
|
||||
name: docFile.name,
|
||||
});
|
||||
const fileId = res.data?.data?.id;
|
||||
if (fileId) apbdesState.edit.form.fileId = fileId;
|
||||
if (fileId) {
|
||||
apbdesState.edit.form.fileId = fileId;
|
||||
}
|
||||
}
|
||||
|
||||
// Jika tidak ada file baru, gunakan ID lama (sudah ada di form)
|
||||
// Pastikan imageId dan fileId tetap ada
|
||||
if (!apbdesState.edit.form.imageId) {
|
||||
return toast.warn('Gambar wajib diunggah');
|
||||
}
|
||||
if (!apbdesState.edit.form.fileId) {
|
||||
return toast.warn('Dokumen wajib diunggah');
|
||||
}
|
||||
|
||||
const success = await apbdesState.edit.update();
|
||||
@@ -194,21 +238,33 @@ function EditAPBDes() {
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
const id = params?.id as string;
|
||||
if (id) {
|
||||
apbdesState.edit.load(id);
|
||||
setImageFile(null);
|
||||
setDocFile(null);
|
||||
setNewItem({
|
||||
kode: '',
|
||||
uraian: '',
|
||||
anggaran: 0,
|
||||
realisasi: 0,
|
||||
level: 1,
|
||||
tipe: 'pendapatan',
|
||||
});
|
||||
toast.info('Form dikembalikan ke data awal');
|
||||
}
|
||||
// Reset ke data original (tahun, imageId, fileId)
|
||||
apbdesState.edit.form = {
|
||||
tahun: originalData.tahun,
|
||||
imageId: originalData.imageId,
|
||||
fileId: originalData.fileId,
|
||||
items: [...apbdesState.edit.form.items], // keep existing items
|
||||
};
|
||||
|
||||
// Reset preview ke data original
|
||||
setPreviewImage(originalData.imageUrl || null);
|
||||
setPreviewDoc(originalData.fileUrl || null);
|
||||
|
||||
// Reset file uploads
|
||||
setImageFile(null);
|
||||
setDocFile(null);
|
||||
|
||||
// Reset new item form
|
||||
setNewItem({
|
||||
kode: '',
|
||||
uraian: '',
|
||||
anggaran: 0,
|
||||
realisasi: 0,
|
||||
level: 1,
|
||||
tipe: 'pendapatan',
|
||||
});
|
||||
|
||||
toast.info('Form dikembalikan ke data awal');
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -82,17 +82,17 @@ function EditDaftarInformasiPublik() {
|
||||
await daftarInformasi.edit.update();
|
||||
router.push('/admin/ppid/daftar-informasi-publik');
|
||||
} catch (error) {
|
||||
console.error('Error updating berita:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui berita');
|
||||
console.error('Error updating daftar informasi:', error);
|
||||
toast.error('Terjadi kesalahan saat memperbarui daftar informasi');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group mb="md">
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||
</Button>
|
||||
<Title order={4} ml="sm" c="dark">
|
||||
Edit Daftar Informasi Publik
|
||||
</Title>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import stateDasarHukumPPID from '../../_state/ppid/dasar_hukum/dasarHukum';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
function Page() {
|
||||
const router = useRouter();
|
||||
@@ -68,7 +69,7 @@ function Page() {
|
||||
lh={{ base: 1.15, md: 1.1 }}
|
||||
fw="bold"
|
||||
c={colors['blue-button']}
|
||||
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listDasarHukum.findById.data.judul) }}
|
||||
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||
/>
|
||||
</GridCol>
|
||||
@@ -77,7 +78,7 @@ function Page() {
|
||||
<Divider my="xl" color={colors['blue-button']} />
|
||||
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listDasarHukum.findById.data.content) }}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import stateProfilePPID from '../../_state/ppid/profile_ppid/profile_PPID';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
function Page() {
|
||||
const router = useRouter();
|
||||
@@ -114,7 +115,7 @@ function Page() {
|
||||
c={colors['blue-button']}
|
||||
lh={1.5}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: item.biodata }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.biodata) }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -129,7 +130,7 @@ function Page() {
|
||||
c={colors['blue-button']}
|
||||
lh={1.5}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: item.riwayat }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.riwayat) }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -145,7 +146,7 @@ function Page() {
|
||||
c={colors['blue-button']}
|
||||
lh={1.5}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: item.pengalaman }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.pengalaman) }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -161,7 +162,7 @@ function Page() {
|
||||
c={colors['blue-button']}
|
||||
lh={1.5}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: item.unggulan }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.unggulan) }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
function PosisiOrganisasiPPID() {
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -100,7 +101,7 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
<Text fz="md" fw={600} lh={1.5} truncate="end" lineClamp={1}>{item.nama}</Text>
|
||||
</TableTd>
|
||||
<TableTd w={200}>
|
||||
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
|
||||
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.deskripsi || '-') }} />
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text fz="md" lh={1.5}>{item.hierarki || '-'}</Text>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import stateVisiMisiPPID from '../../_state/ppid/visi_misi_ppid/visimisiPPID';
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
function VisiMisiPPIDList() {
|
||||
const router = useRouter();
|
||||
@@ -96,7 +97,7 @@ function VisiMisiPPIDList() {
|
||||
</Title>
|
||||
<Text
|
||||
ta={{ base: "center", md: "justify" }}
|
||||
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listVisiMisi.findById.data.visi) }}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
@@ -121,7 +122,7 @@ function VisiMisiPPIDList() {
|
||||
</Title>
|
||||
<Text
|
||||
ta={"justify"}
|
||||
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listVisiMisi.findById.data.misi) }}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
|
||||
@@ -35,35 +35,35 @@ export async function POST(req: Request) {
|
||||
|
||||
// ✅ PERBAIKAN: Gunakan format pesan yang lebih sederhana
|
||||
// Hapus karakter khusus yang bisa bikin masalah
|
||||
const waMessage = `Website Desa Darmasaba\nKode verifikasi Anda ${codeOtp}`;
|
||||
// const waMessage = `Website Desa Darmasaba\nKode verifikasi Anda ${codeOtp}`;
|
||||
|
||||
// // ✅ OPSI 1: Tanpa encoding (coba dulu ini)
|
||||
// const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${waMessage}`;
|
||||
|
||||
// ✅ OPSI 2: Dengan encoding (kalau opsi 1 gagal)
|
||||
const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodeURIComponent(waMessage)}`;
|
||||
// const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodeURIComponent(waMessage)}`;
|
||||
|
||||
// ✅ OPSI 3: Encoding manual untuk URL-safe (alternatif terakhir)
|
||||
// const encodedMessage = waMessage.replace(/\n/g, '%0A').replace(/ /g, '%20');
|
||||
// const waUrl = `https://wa.wibudev.com/code?nom=${nomor}&text=${encodedMessage}`;
|
||||
|
||||
console.log("🔍 Debug WA URL:", waUrl); // Untuk debugging
|
||||
// console.log("🔍 Debug WA URL:", waUrl); // Untuk debugging
|
||||
|
||||
const res = await fetch(waUrl);
|
||||
const sendWa = await res.json();
|
||||
// const res = await fetch(waUrl);
|
||||
// const sendWa = await res.json();
|
||||
|
||||
console.log("📱 WA Response:", sendWa); // Debug response
|
||||
// console.log("📱 WA Response:", sendWa); // Debug response
|
||||
|
||||
if (sendWa.status !== "success") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: "Gagal mengirim OTP via WhatsApp",
|
||||
debug: sendWa // Tampilkan error detail
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
// if (sendWa.status !== "success") {
|
||||
// return NextResponse.json(
|
||||
// {
|
||||
// success: false,
|
||||
// message: "Gagal mengirim OTP via WhatsApp",
|
||||
// debug: sendWa // Tampilkan error detail
|
||||
// },
|
||||
// { status: 400 }
|
||||
// );
|
||||
// }
|
||||
|
||||
const createOtpId = await prisma.kodeOtp.create({
|
||||
data: { nomor, otp: otpNumber, isActive: true },
|
||||
|
||||
Reference in New Issue
Block a user