Compare commits

...

3 Commits

Author SHA1 Message Date
af368eeee0 bump: version 0.1.5 -> 0.1.6 - fix migrasi penduduk schema mismatch
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 16:08:10 +08:00
e104cd8fcc docs: update QWEN.md
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 16:08:01 +08:00
50801e5c8a fix(kependudukan): remove jenisKelamin field and align MigrasiPenduduk with database schema
- Remove jenisKelamin field from API, state, and UI components
- Fix MigrasiPenduduk API to use null instead of undefined for optional fields
- Update create/edit forms to properly handle asal/tujuan fields based on jenis
- Fix DatePickerInput type handling with valueFormat prop
- Update list page to display asal or tujuan conditionally
- Add proper select statements in API responses
- Fix TypeScript type errors in migrasi-penduduk module

Closes: Schema mismatch causing errors when inputting migrasi penduduk data

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 15:53:58 +08:00
9 changed files with 68 additions and 77 deletions

22
QWEN.md
View File

@@ -229,4 +229,24 @@ Common issues and solutions:
3. Test database changes with `bunx prisma db push`
4. Use the integrated Swagger docs at `/api/docs` for API testing
5. Check environment variables are properly configured
6. Verify responsive design on different screen sizes
6. Verify responsive design on different screen sizes
## Qwen Added Memories
- **GitHub Workflow Execution**: Project ini memiliki 3 workflow GitHub Action:
1. `publish.yml` - Build & push Docker image ke GHCR (manual trigger, butuh input: stack_env + tag)
2. `re-pull.yml` - Re-pull Docker image di Portainer (manual trigger, butuh input: stack_name + stack_env)
3. `docker-publish.yml` - Auto build & push saat ada tag versi v*
Workflow bisa dijalankan via GitHub CLI: `gh workflow run <nama.yml> -f param=value --ref branch`
Setelah commit ke branch deployment (dev/stg/prod), otomatis trigger workflow publish + re-pull untuk deploy ke server.
- **Deployment Workflow Sistematis**:
1. **Version Bump** - Update `version` di `package.json` sebelum deploy (ikuti semver: major.minor.patch)
2. **Commit** - Commit perubahan + version bump dengan pesan yang jelas
3. **Push ke Branch** - Push ke branch target (biasanya `stg` untuk staging atau `prod` untuk production)
4. **Trigger Publish** - Jalankan `gh workflow run publish.yml --ref <branch> -f stack_env=<env> -f tag=<version>`
5. **Trigger Re-Pull** - Jalankan `gh workflow run re-pull.yml -f stack_name=desa-darmasaba -f stack_env=<env>`
6. **Verifikasi** - Cek workflow berhasil dan aplikasi berjalan
Branch deployment: `stg` (staging) atau `prod` (production)
Version format di package.json: `"version": "major.minor.patch"`

View File

@@ -1,6 +1,6 @@
{
"name": "desa-darmasaba",
"version": "0.1.5",
"version": "0.1.6",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -9,7 +9,6 @@ const templateMigrasiPenduduk = z.object({
tanggal: z.string().min(1, "Tanggal harus diisi"),
asalTujuan: z.string().min(1, "Asal/Tujuan harus diisi"),
alasan: z.string().optional(),
jenisKelamin: z.string().optional(),
});
const migrasiPenduduk = proxy({
@@ -20,7 +19,6 @@ const migrasiPenduduk = proxy({
tanggal: "",
asalTujuan: "",
alasan: "",
jenisKelamin: "",
},
loading: false,
async create() {
@@ -38,7 +36,7 @@ const migrasiPenduduk = proxy({
const id = res.data?.data?.id;
if (id) {
toast.success("Sukses menambahkan data migrasi penduduk");
migrasiPenduduk.create.form = { jenis: "", nama: "", tanggal: "", asalTujuan: "", alasan: "", jenisKelamin: "" };
migrasiPenduduk.create.form = { jenis: "", nama: "", tanggal: "", asalTujuan: "", alasan: "" };
migrasiPenduduk.findMany.load();
return id;
}
@@ -116,7 +114,6 @@ const migrasiPenduduk = proxy({
tanggal: "",
asalTujuan: "",
alasan: "",
jenisKelamin: "",
},
loading: false,
async submit() {
@@ -132,7 +129,6 @@ const migrasiPenduduk = proxy({
tanggal: this.form.tanggal,
asalTujuan: this.form.asalTujuan,
alasan: this.form.alasan,
jenisKelamin: this.form.jenisKelamin,
};
const cek = templateMigrasiPenduduk.safeParse(formData);

View File

@@ -28,7 +28,6 @@ interface MigrasiPendudukForm {
tanggal: string;
asalTujuan: string;
alasan: string;
jenisKelamin: string;
}
export default function EditMigrasiPenduduk() {
@@ -42,7 +41,6 @@ export default function EditMigrasiPenduduk() {
tanggal: '',
asalTujuan: '',
alasan: '',
jenisKelamin: '',
});
const [originalData, setOriginalData] = useState<MigrasiPendudukForm>({
jenis: '',
@@ -50,7 +48,6 @@ export default function EditMigrasiPenduduk() {
tanggal: '',
asalTujuan: '',
alasan: '',
jenisKelamin: '',
});
const jenisOptions = [
@@ -58,11 +55,6 @@ export default function EditMigrasiPenduduk() {
{ value: 'KELUAR', label: 'Keluar' },
];
const jenisKelaminOptions = [
{ value: 'L', label: 'Laki-laki' },
{ value: 'P', label: 'Perempuan' },
];
const isFormValid = () => {
return (
formData.jenis?.trim() !== '' &&
@@ -81,23 +73,23 @@ export default function EditMigrasiPenduduk() {
stateMigrasiPenduduk.update.id = id;
await stateMigrasiPenduduk.findUnique.load(id);
const data = stateMigrasiPenduduk.findUnique.data as MigrasiPendudukForm | null;
const data = stateMigrasiPenduduk.findUnique.data as any;
if (data) {
const asalTujuan = data.jenis === 'MASUK' ? (data.asal || '') : (data.tujuan || '');
setFormData({
jenis: data.jenis ?? '',
nama: data.nama ?? '',
tanggal: data.tanggal ?? '',
asalTujuan: data.asalTujuan ?? '',
tanggal: data.tanggal ? new Date(data.tanggal).toISOString().split('T')[0] : '',
asalTujuan: asalTujuan,
alasan: data.alasan ?? '',
jenisKelamin: data.jenisKelamin ?? '',
});
setOriginalData({
jenis: data.jenis ?? '',
nama: data.nama ?? '',
tanggal: data.tanggal ?? '',
asalTujuan: data.asalTujuan ?? '',
tanggal: data.tanggal ? new Date(data.tanggal).toISOString().split('T')[0] : '',
asalTujuan: asalTujuan,
alasan: data.alasan ?? '',
jenisKelamin: data.jenisKelamin ?? '',
});
}
} catch (error) {
@@ -114,7 +106,7 @@ export default function EditMigrasiPenduduk() {
const handleChangeText = useCallback(
(field: keyof MigrasiPendudukForm) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData((prev) => ({ ...prev, [field]: e.currentTarget.value as never }));
setFormData((prev) => ({ ...prev, [field]: e.currentTarget.value }));
},
[]
);
@@ -122,7 +114,7 @@ export default function EditMigrasiPenduduk() {
const handleChangeSelect = useCallback(
(field: keyof MigrasiPendudukForm) =>
(value: string | null) => {
setFormData((prev) => ({ ...prev, [field]: (value || '') as never }));
setFormData((prev) => ({ ...prev, [field]: value || '' }));
},
[]
);
@@ -134,7 +126,6 @@ export default function EditMigrasiPenduduk() {
tanggal: originalData.tanggal,
asalTujuan: originalData.asalTujuan,
alasan: originalData.alasan,
jenisKelamin: originalData.jenisKelamin,
});
toast.info("Form dikembalikan ke data awal");
};
@@ -211,6 +202,7 @@ export default function EditMigrasiPenduduk() {
tanggal: val || '',
}));
}}
valueFormat="YYYY-MM-DD"
required
/>
@@ -231,14 +223,6 @@ export default function EditMigrasiPenduduk() {
minRows={2}
/>
<Select
label="Jenis Kelamin"
placeholder="Pilih jenis kelamin"
data={jenisKelaminOptions}
value={formData.jenisKelamin}
onChange={handleChangeSelect('jenisKelamin')}
/>
<Group justify="flex-end">
<Button
variant="outline"

View File

@@ -31,11 +31,6 @@ function CreateMigrasiPenduduk() {
{ value: 'KELUAR', label: 'Keluar' },
];
const jenisKelaminOptions = [
{ value: 'L', label: 'Laki-laki' },
{ value: 'P', label: 'Perempuan' },
];
const isFormValid = () => {
return (
stateMigrasiPenduduk.create.form.jenis?.trim() !== '' &&
@@ -52,12 +47,12 @@ function CreateMigrasiPenduduk() {
tanggal: '',
asalTujuan: '',
alasan: '',
jenisKelamin: '',
};
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
const id = await stateMigrasiPenduduk.create.create();
if (id) {
resetForm();
@@ -126,6 +121,7 @@ function CreateMigrasiPenduduk() {
onChange={(val: string | null) => {
stateMigrasiPenduduk.create.form.tanggal = val || '';
}}
valueFormat="YYYY-MM-DD"
required
/>
@@ -150,16 +146,6 @@ function CreateMigrasiPenduduk() {
minRows={2}
/>
<Select
label="Jenis Kelamin"
placeholder="Pilih jenis kelamin"
data={jenisKelaminOptions}
value={stateMigrasiPenduduk.create.form.jenisKelamin}
onChange={(val) => {
stateMigrasiPenduduk.create.form.jenisKelamin = val || '';
}}
/>
<Group justify="right">
<Button
variant="outline"

View File

@@ -57,9 +57,9 @@ function ListMigrasiPenduduk({ search, year }: { search: string; year?: number }
jenis: string;
nama: string;
tanggal: string;
asalTujuan: string;
asal: string | null;
tujuan: string | null;
alasan: string | null;
jenisKelamin: string | null;
};
const router = useRouter();
@@ -142,16 +142,17 @@ function ListMigrasiPenduduk({ search, year }: { search: string; year?: number }
<TableTr>
<TableTh style={{ width: '10%' }}>Jenis</TableTh>
<TableTh style={{ width: '20%' }}>Nama</TableTh>
<TableTh style={{ width: '12%' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%' }}>Asal/Tujuan</TableTh>
<TableTh style={{ width: '10%' }}>L/P</TableTh>
<TableTh style={{ width: '15%' }}>Tanggal</TableTh>
<TableTh style={{ width: '25%' }}>Asal/Tujuan</TableTh>
<TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh style={{ width: '10%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
filteredData.map((item) => {
const asalTujuan = item.jenis === 'MASUK' ? (item.asal || '-') : (item.tujuan || '-');
return (
<TableTr key={item.id}>
<TableTd>
<Text
@@ -164,8 +165,7 @@ function ListMigrasiPenduduk({ search, year }: { search: string; year?: number }
</TableTd>
<TableTd>{item.nama}</TableTd>
<TableTd>{formatTanggal(item.tanggal)}</TableTd>
<TableTd>{item.asalTujuan}</TableTd>
<TableTd>{item.jenisKelamin || '-'}</TableTd>
<TableTd>{asalTujuan}</TableTd>
<TableTd>
<Button
variant="light"
@@ -197,10 +197,11 @@ function ListMigrasiPenduduk({ search, year }: { search: string; year?: number }
</Button>
</TableTd>
</TableTr>
))
);
})
) : (
<TableTr>
<TableTd colSpan={7}>
<TableTd colSpan={6}>
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data migrasi penduduk yang cocok
@@ -217,7 +218,9 @@ function ListMigrasiPenduduk({ search, year }: { search: string; year?: number }
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item) => (
filteredData.map((item) => {
const asalTujuan = item.jenis === 'MASUK' ? (item.asal || '-') : (item.tujuan || '-');
return (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
@@ -250,10 +253,10 @@ function ListMigrasiPenduduk({ search, year }: { search: string; year?: number }
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Asal/Tujuan
{item.jenis === 'MASUK' ? 'Asal' : 'Tujuan'}
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.asalTujuan}
{asalTujuan}
</Text>
</Box>
{item.alasan && (
@@ -266,14 +269,6 @@ function ListMigrasiPenduduk({ search, year }: { search: string; year?: number }
</Text>
</Box>
)}
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jenis Kelamin
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.jenisKelamin || '-'}
</Text>
</Box>
<Group justify="flex-end" gap="xs">
<Button
variant="light"
@@ -304,7 +299,8 @@ function ListMigrasiPenduduk({ search, year }: { search: string; year?: number }
</Group>
</Stack>
</Paper>
))
);
})
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>

View File

@@ -19,8 +19,8 @@ export default async function migrasiPendudukCreate(context: Context) {
jenis: body.jenis as 'MASUK' | 'KELUAR',
nama: body.nama,
tanggal: new Date(body.tanggal),
asal: isMasuk ? body.asalTujuan : undefined,
tujuan: !isMasuk ? body.asalTujuan : undefined,
asal: isMasuk ? body.asalTujuan : null,
tujuan: !isMasuk ? body.asalTujuan : null,
alasan: body.alasan,
},
select: {
@@ -28,6 +28,8 @@ export default async function migrasiPendudukCreate(context: Context) {
jenis: true,
nama: true,
tanggal: true,
asal: true,
tujuan: true,
alasan: true,
}
});

View File

@@ -21,7 +21,6 @@ const MigrasiPenduduk = new Elysia({
tanggal: t.String(),
asalTujuan: t.String(),
alasan: t.Optional(t.String()),
jenisKelamin: t.Optional(t.String()),
}),
})
.put("/:id", migrasiPendudukUpdate, {
@@ -34,7 +33,6 @@ const MigrasiPenduduk = new Elysia({
tanggal: t.String(),
asalTujuan: t.String(),
alasan: t.Optional(t.String()),
jenisKelamin: t.Optional(t.String()),
}),
})
.delete("/del/:id", migrasiPendudukDelete, {

View File

@@ -38,10 +38,19 @@ export default async function migrasiPendudukUpdate(context: Context) {
jenis: jenis as 'MASUK' | 'KELUAR',
nama,
tanggal: new Date(tanggal),
asal: jenis === 'MASUK' ? asalTujuan : undefined,
tujuan: jenis === 'KELUAR' ? asalTujuan : undefined,
asal: jenis === 'MASUK' ? asalTujuan : null,
tujuan: jenis === 'KELUAR' ? asalTujuan : null,
alasan,
},
select: {
id: true,
jenis: true,
nama: true,
tanggal: true,
asal: true,
tujuan: true,
alasan: true,
},
})
return {