Refactor ExpenseFormModal and CategoryFormModal to utilize useFormDialog for improved form handling. Replace MediaImageUpload with ImageUploadField for photo uploads, enhancing user experience. Remove unused ImageUploadField component to streamline codebase.

This commit is contained in:
Yoga Pangestu 2026-06-12 16:27:45 +07:00
parent 45e06783db
commit c734d18018
6 changed files with 95 additions and 52 deletions

View File

@ -1,9 +1,8 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { Save } from '@lucide/vue';
import { computed, watch } from 'vue';
import { computed, ref } from 'vue';
import { toast } from 'vue-sonner';
import MediaImageUpload from '@/components/media/MediaImageUpload.vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
@ -19,12 +18,13 @@ import {
FieldLabel,
FieldSet,
} from '@/components/ui/field';
import { ImageUploadField } from '@/components/ui/image-upload-field';
import { RupiahInput } from '@/components/ui/rupiah-input';
import { Textarea } from '@/components/ui/textarea';
import { useFormDialog } from '@/composables/useFormDialog';
import { formErrors } from '@/lib/form';
import { parseRupiah } from '@/lib/rupiah';
import type { ExpenseListItem } from '@/types/expense';
import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media';
const open = defineModel<boolean>('open', { default: false });
@ -34,15 +34,20 @@ const props = defineProps<{
const isEditing = computed(() => props.expense != null);
const existingPhotoId = ref<number | null>(null);
const currentPhotoUrl = computed(() => props.expense?.photos?.[0]?.url ?? null);
const form = useForm({
amount: '',
description: '',
media: createMediaUploadState(),
photo: null as File | null,
});
function resetForm() {
form.reset();
form.media = createMediaUploadState();
form.photo = null;
existingPhotoId.value = null;
form.clearErrors();
}
@ -55,22 +60,14 @@ function populateForm(expense: ExpenseListItem | null | undefined) {
form.amount = String(expense.amount);
form.description = expense.description;
form.media = createMediaUploadState(expense.photos ?? []);
existingPhotoId.value = expense.photos?.[0]?.id ?? null;
}
watch(
() => props.expense,
(expense) => {
populateForm(expense);
},
);
watch(open, (isOpen) => {
if (isOpen) {
populateForm(props.expense);
} else {
resetForm();
}
useFormDialog({
open,
source: () => props.expense,
populate: populateForm,
reset: resetForm,
});
function buildFormData(forUpdate: boolean): FormData {
@ -82,12 +79,21 @@ function buildFormData(forUpdate: boolean): FormData {
formData.append('amount', parseRupiah(form.amount));
formData.append('description', form.description);
appendRootPhotosToFormData(formData, form.media);
if (form.photo) {
formData.append('photos[]', form.photo);
if (existingPhotoId.value) {
formData.append('remove_media_ids[]', String(existingPhotoId.value));
}
}
return formData;
}
function submit() {
const payload = buildFormData(isEditing.value);
const options = {
forceFormData: true,
preserveScroll: true,
@ -99,13 +105,13 @@ function submit() {
},
};
const payload = buildFormData(isEditing.value);
if (isEditing.value && props.expense) {
form.transform(() => payload).post(`/admin/finance/expenses/${props.expense.id}`, options);
} else {
form.transform(() => payload).post('/admin/finance/expenses', options);
form.transform(() => payload).put(`/admin/finance/expenses/${props.expense.id}`, options);
return;
}
form.transform(() => payload).post('/admin/finance/expenses', options);
}
</script>
@ -130,8 +136,8 @@ function submit() {
placeholder="Contoh: Pembelian perlengkapan toko" rows="3" />
<FieldError :errors="formErrors(form, 'description')" />
</Field>
<MediaImageUpload id="expense-photos" v-model="form.media" label="Foto Bukti" required
:errors="formErrors(form, 'photos')" />
<ImageUploadField id="expense-photos" v-model="form.photo" label="Foto Bukti" required
:current-url="currentPhotoUrl" :errors="formErrors(form, 'photos')" />
</FieldSet>
</FieldGroup>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { Save } from '@lucide/vue';
import { computed, watch } from 'vue';
import { computed } from 'vue';
import { toast } from 'vue-sonner';
import { Button } from '@/components/ui/button';
import {
@ -19,6 +19,7 @@ import {
FieldSet,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { useFormDialog } from '@/composables/useFormDialog';
import { formErrors } from '@/lib/form';
import type { CategoryFormData, CategoryListItem } from '@/types/category';
@ -49,19 +50,11 @@ function populateForm(category: CategoryListItem | null | undefined) {
form.name = category.name;
}
watch(
() => props.category,
(category) => {
populateForm(category);
},
);
watch(open, (isOpen) => {
if (isOpen) {
populateForm(props.category);
} else {
resetForm();
}
useFormDialog({
open,
source: () => props.category,
populate: populateForm,
reset: resetForm,
});
function submit() {
@ -77,9 +70,11 @@ function submit() {
if (isEditing.value && props.category) {
form.put(`/admin/master/categories/${props.category.id}`, options);
} else {
form.post('/admin/master/categories', options);
return;
}
form.post('/admin/master/categories', options);
}
</script>

View File

@ -5,12 +5,12 @@ import { toast } from 'vue-sonner';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field';
import { ImageUploadField } from '@/components/ui/image-upload-field';
import { Input } from '@/components/ui/input';
import { PhoneNumberInput } from '@/components/ui/phone-number-input';
import { Textarea } from '@/components/ui/textarea';
import { formErrors } from '@/lib/form';
import type { SystemSettingsData } from '@/types/setting';
import ImageUploadField from './ImageUploadField.vue';
const props = defineProps<{
data: SystemSettingsData;

View File

@ -14,6 +14,7 @@ const props = defineProps<{
errors?: string[];
accept?: string;
required?: boolean;
previewClass?: string;
}>();
const model = defineModel<File | null>();
@ -55,20 +56,34 @@ function clearFile() {
</FieldDescription>
<div class="space-y-3">
<div v-if="displayUrl" :class="cn(
'relative overflow-hidden rounded-lg border bg-muted/30',
id === 'favicon' ? 'size-20' : 'aspect-video max-w-md',
)">
<div
v-if="displayUrl"
:class="cn(
'relative overflow-hidden rounded-lg border bg-muted/30',
previewClass ?? 'aspect-video max-w-md',
)"
>
<img :src="displayUrl" :alt="label" class="size-full object-cover">
<Button v-if="model" type="button" variant="secondary" size="icon" class="absolute top-2 right-2 size-7"
@click="clearFile">
<Button
v-if="model"
type="button"
variant="secondary"
size="icon"
class="absolute top-2 right-2 size-7"
@click="clearFile"
>
<X class="size-4" />
</Button>
</div>
<div class="flex items-center gap-2">
<Input :id="id" type="file" :accept="accept ?? 'image/*'" class="max-w-md cursor-pointer"
@change="onFileChange" />
<Input
:id="id"
type="file"
:accept="accept ?? 'image/*'"
class="max-w-md cursor-pointer"
@change="onFileChange"
/>
<ImagePlus v-if="!displayUrl" class="text-muted-foreground size-5 shrink-0" />
</div>
</div>

View File

@ -0,0 +1 @@
export { default as ImageUploadField } from './ImageUploadField.vue';

View File

@ -0,0 +1,26 @@
import { watch } from 'vue';
import type { Ref } from 'vue';
interface UseFormDialogOptions<T> {
open: Ref<boolean>;
source: () => T;
populate: (value: T) => void;
reset: () => void;
}
export function useFormDialog<T>({
open,
source,
populate,
reset,
}: UseFormDialogOptions<T>) {
watch(source, populate);
watch(open, (isOpen) => {
if (isOpen) {
populate(source());
} else {
reset();
}
});
}