feat(create): add realisasi awal input di create page
Features: - Add realisasiAwal field to ItemForm type - Add NumberInput for realisasi awal (optional) - Update table preview to show realisasi awal - Update state to send realisasiAwal to API - Update API create to handle realisasiAwal: * Create APBDesItem with totalRealisasi = realisasiAwal * Auto-create first RealisasiItem if realisasiAwal > 0 * Auto-calculate selisih and persentase UX Improvements: - User can input initial realization during create - Optional field with clear label and description - Auto-calculation of percentages on backend - Single transaction for item + first realisasi Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -5,11 +5,12 @@ import { toast } from "react-toastify";
|
|||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
// --- Zod Schema untuk APBDes Item (tanpa field kalkulasi) ---
|
// --- Zod Schema untuk APBDes Item (dengan realisasiAwal opsional) ---
|
||||||
const ApbdesItemSchema = z.object({
|
const ApbdesItemSchema = z.object({
|
||||||
kode: z.string().min(1, "Kode wajib diisi"),
|
kode: z.string().min(1, "Kode wajib diisi"),
|
||||||
uraian: z.string().min(1, "Uraian wajib diisi"),
|
uraian: z.string().min(1, "Uraian wajib diisi"),
|
||||||
anggaran: z.number().min(0, "Anggaran tidak boleh negatif"),
|
anggaran: z.number().min(0, "Anggaran tidak boleh negatif"),
|
||||||
|
realisasiAwal: z.number().min(0).optional(), // Realisasi pertama saat create
|
||||||
level: z.number().int().min(1).max(3),
|
level: z.number().int().min(1).max(3),
|
||||||
tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(),
|
tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(),
|
||||||
});
|
});
|
||||||
@@ -91,7 +92,21 @@ const apbdes = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const res = await ApiFetch.api.landingpage.apbdes["create"].post(parsed.data);
|
|
||||||
|
// Extract realisasiAwal dari items
|
||||||
|
const itemsWithRealisasi = this.form.items.map(item => ({
|
||||||
|
kode: item.kode,
|
||||||
|
uraian: item.uraian,
|
||||||
|
anggaran: item.anggaran,
|
||||||
|
level: item.level,
|
||||||
|
tipe: item.tipe,
|
||||||
|
realisasiAwal: item.realisasiAwal || 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.landingpage.apbdes["create"].post({
|
||||||
|
...parsed.data,
|
||||||
|
items: itemsWithRealisasi,
|
||||||
|
});
|
||||||
|
|
||||||
if (res.data?.success) {
|
if (res.data?.success) {
|
||||||
toast.success("APBDes berhasil dibuat");
|
toast.success("APBDes berhasil dibuat");
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type ItemForm = {
|
|||||||
kode: string;
|
kode: string;
|
||||||
uraian: string;
|
uraian: string;
|
||||||
anggaran: number;
|
anggaran: number;
|
||||||
|
realisasiAwal?: number; // Realisasi pertama saat create
|
||||||
level: number;
|
level: number;
|
||||||
tipe: 'pendapatan' | 'belanja' | 'pembiayaan';
|
tipe: 'pendapatan' | 'belanja' | 'pembiayaan';
|
||||||
};
|
};
|
||||||
@@ -60,6 +61,7 @@ function CreateAPBDes() {
|
|||||||
kode: '',
|
kode: '',
|
||||||
uraian: '',
|
uraian: '',
|
||||||
anggaran: 0,
|
anggaran: 0,
|
||||||
|
realisasiAwal: 0,
|
||||||
level: 1,
|
level: 1,
|
||||||
tipe: 'pendapatan',
|
tipe: 'pendapatan',
|
||||||
});
|
});
|
||||||
@@ -78,6 +80,7 @@ function CreateAPBDes() {
|
|||||||
kode: '',
|
kode: '',
|
||||||
uraian: '',
|
uraian: '',
|
||||||
anggaran: 0,
|
anggaran: 0,
|
||||||
|
realisasiAwal: 0,
|
||||||
level: 1,
|
level: 1,
|
||||||
tipe: 'pendapatan',
|
tipe: 'pendapatan',
|
||||||
});
|
});
|
||||||
@@ -124,7 +127,7 @@ function CreateAPBDes() {
|
|||||||
|
|
||||||
// Tambahkan item ke state
|
// Tambahkan item ke state
|
||||||
const handleAddItem = () => {
|
const handleAddItem = () => {
|
||||||
const { kode, uraian, anggaran, level, tipe } = newItem;
|
const { kode, uraian, anggaran, realisasiAwal, level, tipe } = newItem;
|
||||||
if (!kode || !uraian) {
|
if (!kode || !uraian) {
|
||||||
return toast.warn("Kode dan uraian wajib diisi");
|
return toast.warn("Kode dan uraian wajib diisi");
|
||||||
}
|
}
|
||||||
@@ -135,6 +138,7 @@ function CreateAPBDes() {
|
|||||||
kode,
|
kode,
|
||||||
uraian,
|
uraian,
|
||||||
anggaran,
|
anggaran,
|
||||||
|
realisasiAwal: realisasiAwal || 0,
|
||||||
level,
|
level,
|
||||||
tipe: finalTipe,
|
tipe: finalTipe,
|
||||||
});
|
});
|
||||||
@@ -144,6 +148,7 @@ function CreateAPBDes() {
|
|||||||
kode: '',
|
kode: '',
|
||||||
uraian: '',
|
uraian: '',
|
||||||
anggaran: 0,
|
anggaran: 0,
|
||||||
|
realisasiAwal: 0,
|
||||||
level: 1,
|
level: 1,
|
||||||
tipe: 'pendapatan',
|
tipe: 'pendapatan',
|
||||||
});
|
});
|
||||||
@@ -418,6 +423,14 @@ function CreateAPBDes() {
|
|||||||
thousandSeparator
|
thousandSeparator
|
||||||
min={0}
|
min={0}
|
||||||
/>
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label="Realisasi Awal (Rp) - Opsional"
|
||||||
|
value={newItem.realisasiAwal}
|
||||||
|
onChange={(val) => setNewItem({ ...newItem, realisasiAwal: Number(val) || 0 })}
|
||||||
|
thousandSeparator
|
||||||
|
min={0}
|
||||||
|
description="Isi jika sudah ada realisasi saat create"
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={16} />}
|
leftSection={<IconPlus size={16} />}
|
||||||
@@ -439,17 +452,19 @@ function CreateAPBDes() {
|
|||||||
<th>Kode</th>
|
<th>Kode</th>
|
||||||
<th>Uraian</th>
|
<th>Uraian</th>
|
||||||
<th>Anggaran</th>
|
<th>Anggaran</th>
|
||||||
|
<th>Realisasi Awal</th>
|
||||||
<th>Level</th>
|
<th>Level</th>
|
||||||
<th>Tipe</th>
|
<th>Tipe</th>
|
||||||
<th style={{ width: 50 }}>Aksi</th>
|
<th style={{ width: 50 }}>Aksi</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{stateAPBDes.create.form.items.map((item, idx) => (
|
{stateAPBDes.create.form.items.map((item: any, idx) => (
|
||||||
<tr key={idx}>
|
<tr key={idx}>
|
||||||
<td><Text size="sm" fw={500}>{item.kode}</Text></td>
|
<td><Text size="sm" fw={500}>{item.kode}</Text></td>
|
||||||
<td>{item.uraian}</td>
|
<td>{item.uraian}</td>
|
||||||
<td>{item.anggaran.toLocaleString('id-ID')}</td>
|
<td>{item.anggaran.toLocaleString('id-ID')}</td>
|
||||||
|
<td>{(item.realisasiAwal || 0).toLocaleString('id-ID')}</td>
|
||||||
<td>
|
<td>
|
||||||
<Badge size="sm" color={item.level === 1 ? 'blue' : item.level === 2 ? 'green' : 'grape'}>
|
<Badge size="sm" color={item.level === 1 ? 'blue' : item.level === 2 ? 'green' : 'grape'}>
|
||||||
L{item.level}
|
L{item.level}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type APBDesItemInput = {
|
|||||||
anggaran: number;
|
anggaran: number;
|
||||||
level: number;
|
level: number;
|
||||||
tipe?: string | null;
|
tipe?: string | null;
|
||||||
|
realisasiAwal?: number; // Realisasi pertama saat create
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormCreate = {
|
type FormCreate = {
|
||||||
@@ -55,14 +56,15 @@ export default async function apbdesCreate(context: Context) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create items dengan auto-calculate totalRealisasi=0, selisih, persentase
|
// Create items dengan auto-calculate totalRealisasi, selisih, persentase
|
||||||
const items = await Promise.all(
|
const items = await Promise.all(
|
||||||
body.items.map(item => {
|
body.items.map(async item => {
|
||||||
const anggaran = item.anggaran;
|
const anggaran = item.anggaran;
|
||||||
const totalRealisasi = 0; // Belum ada realisasi saat create
|
const realisasiAwal = item.realisasiAwal || 0;
|
||||||
const selisih = totalRealisasi - anggaran;
|
|
||||||
const persentase = anggaran > 0 ? (totalRealisasi / anggaran) * 100 : 0;
|
// Jika ada realisasiAwal, buat realisasi item pertama
|
||||||
|
let totalRealisasi = realisasiAwal;
|
||||||
|
|
||||||
const itemData = {
|
const itemData = {
|
||||||
kode: item.kode,
|
kode: item.kode,
|
||||||
uraian: item.uraian,
|
uraian: item.uraian,
|
||||||
@@ -70,15 +72,29 @@ export default async function apbdesCreate(context: Context) {
|
|||||||
level: item.level,
|
level: item.level,
|
||||||
tipe: item.tipe || null,
|
tipe: item.tipe || null,
|
||||||
totalRealisasi,
|
totalRealisasi,
|
||||||
selisih,
|
selisih: totalRealisasi - anggaran,
|
||||||
persentase,
|
persentase: anggaran > 0 ? (totalRealisasi / anggaran) * 100 : 0,
|
||||||
apbdesId: apbdes.id,
|
apbdesId: apbdes.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
return prisma.aPBDesItem.create({
|
const createdItem = await prisma.aPBDesItem.create({
|
||||||
data: itemData,
|
data: itemData,
|
||||||
select: { id: true, kode: true },
|
select: { id: true, kode: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Jika ada realisasiAwal, buat realisasi item pertama
|
||||||
|
if (realisasiAwal > 0) {
|
||||||
|
await prisma.realisasiItem.create({
|
||||||
|
data: {
|
||||||
|
apbdesItemId: createdItem.id,
|
||||||
|
jumlah: realisasiAwal,
|
||||||
|
tanggal: new Date(),
|
||||||
|
keterangan: 'Realisasi awal saat create APBDes',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdItem;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user