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:
parent
45e06783db
commit
c734d18018
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
1
resources/js/components/ui/image-upload-field/index.ts
Normal file
1
resources/js/components/ui/image-upload-field/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as ImageUploadField } from './ImageUploadField.vue';
|
||||
26
resources/js/composables/useFormDialog.ts
Normal file
26
resources/js/composables/useFormDialog.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user