diff --git a/package-lock.json b/package-lock.json index 805c2a8..45b5fcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.0.1", + "dropzone-vue": "^0.1.11", "laravel-vite-plugin": "^3.1", "reka-ui": "^2.9.10", "tailwind-merge": "^3.6.0", @@ -6176,6 +6177,18 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.49.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", @@ -6954,6 +6967,19 @@ "node": ">=0.10.0" } }, + "node_modules/dropzone-vue": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/dropzone-vue/-/dropzone-vue-0.1.11.tgz", + "integrity": "sha512-+XZfKbE2n8FpEoxSTYl9Us4z4qRcRrX24+/uMQxhh4mQuEV1OFAD7v3X2/10ulrRab6wYf5Nv/8pBYchc1ZQyQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "core-js": "^3.9.1", + "vue": "^3.0.5" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index e3854a4..d0b1d21 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.0.1", + "dropzone-vue": "^0.1.11", "laravel-vite-plugin": "^3.1", "reka-ui": "^2.9.10", "tailwind-merge": "^3.6.0", diff --git a/resources/js/components/media/MediaDropzone.vue b/resources/js/components/media/MediaDropzone.vue new file mode 100644 index 0000000..cee4063 --- /dev/null +++ b/resources/js/components/media/MediaDropzone.vue @@ -0,0 +1,633 @@ + + + + + diff --git a/resources/js/pages/admin/finance/cash/form/CashTransactionFormModal.vue b/resources/js/pages/admin/finance/cash/form/CashTransactionFormModal.vue index f5903b7..e114437 100644 --- a/resources/js/pages/admin/finance/cash/form/CashTransactionFormModal.vue +++ b/resources/js/pages/admin/finance/cash/form/CashTransactionFormModal.vue @@ -3,8 +3,8 @@ import { useForm } from '@inertiajs/vue3'; import { Save } from '@lucide/vue'; import { computed, ref } from 'vue'; import { toast } from 'vue-sonner'; -import ImageUploadField from '@/components/form/image-upload-field/ImageUploadField.vue'; import { RupiahInput } from '@/components/form/rupiah-input'; +import MediaDropzone from '@/components/media/MediaDropzone.vue'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -26,8 +26,9 @@ import { CashTransactionType } from '@/constants/cash-transaction-type'; import { FIELD_LIMITS } from '@/lib/field-limits'; import { formErrors } from '@/lib/form'; import { parseRupiah } from '@/lib/rupiah'; -import type { CashTransactionFormData, CashTransactionListItem } from '@/types/cash'; -import { appendPhotosToFormData } from '@/types/media'; +import type { CashTransactionListItem } from '@/types/cash'; +import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media'; +import type { MediaUploadState } from '@/types/media'; const open = defineModel('open', { default: false }); @@ -46,29 +47,16 @@ const currentMode = computed(() => { }); const isWithdrawal = computed(() => currentMode.value === CashTransactionType.WITHDRAWAL); -const existingPhotoId = ref(null); +const photoState = ref(createMediaUploadState()); -const currentPhotoUrl = computed(() => props.transaction?.photos?.[0]?.url ?? null); - -const form = useForm({ +const form = useForm({ amount: '', description: '', - photos: [], - remove_media_ids: [], -}); - -const photoFile = computed({ - get: () => form.photos[0] ?? null, - set: (file) => { - form.photos = file ? [file] : []; - }, }); function resetForm() { form.reset(); - form.photos = []; - form.remove_media_ids = []; - existingPhotoId.value = null; + photoState.value = createMediaUploadState(); form.clearErrors(); } @@ -81,7 +69,7 @@ function populateForm(transaction: CashTransactionListItem | null | undefined) { form.amount = String(transaction.amount); form.description = transaction.description; - existingPhotoId.value = transaction.photos?.[0]?.id ?? null; + photoState.value = createMediaUploadState(transaction.photos ?? []); } useFormDialog({ @@ -101,13 +89,7 @@ function buildFormData(forUpdate: boolean): FormData { formData.append('amount', parseRupiah(form.amount)); formData.append('description', form.description); - const removeMediaIds = [...form.remove_media_ids]; - - if (form.photos.length > 0 && existingPhotoId.value !== null) { - removeMediaIds.push(existingPhotoId.value); - } - - appendPhotosToFormData(formData, form.photos, removeMediaIds); + appendRootPhotosToFormData(formData, photoState.value); return formData; } @@ -173,8 +155,8 @@ const placeholder = computed(() => rows="3" :maxlength="FIELD_LIMITS.description" /> - + diff --git a/resources/js/pages/admin/finance/expenses/form/ExpenseFormModal.vue b/resources/js/pages/admin/finance/expenses/form/ExpenseFormModal.vue index b8c9d6a..0d7a1c2 100644 --- a/resources/js/pages/admin/finance/expenses/form/ExpenseFormModal.vue +++ b/resources/js/pages/admin/finance/expenses/form/ExpenseFormModal.vue @@ -3,8 +3,8 @@ import { useForm } from '@inertiajs/vue3'; import { Save } from '@lucide/vue'; import { computed, ref } from 'vue'; import { toast } from 'vue-sonner'; -import { ImageUploadField } from '@/components/form/image-upload-field'; import { RupiahInput } from '@/components/form/rupiah-input'; +import MediaDropzone from '@/components/media/MediaDropzone.vue'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -25,8 +25,9 @@ import { useFormDialog } from '@/composables/useFormDialog'; import { FIELD_LIMITS } from '@/lib/field-limits'; import { formErrors } from '@/lib/form'; import { parseRupiah } from '@/lib/rupiah'; -import type { ExpenseFormData, ExpenseListItem } from '@/types/expense'; -import { appendPhotosToFormData } from '@/types/media'; +import type { ExpenseListItem } from '@/types/expense'; +import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media'; +import type { MediaUploadState } from '@/types/media'; const open = defineModel('open', { default: false }); @@ -36,29 +37,16 @@ const props = defineProps<{ const isEditing = computed(() => props.expense != null); -const existingPhotoId = ref(null); +const photoState = ref(createMediaUploadState()); -const currentPhotoUrl = computed(() => props.expense?.photos?.[0]?.url ?? null); - -const form = useForm({ +const form = useForm({ amount: '', description: '', - photos: [], - remove_media_ids: [], -}); - -const photoFile = computed({ - get: () => form.photos[0] ?? null, - set: (file) => { - form.photos = file ? [file] : []; - }, }); function resetForm() { form.reset(); - form.photos = []; - form.remove_media_ids = []; - existingPhotoId.value = null; + photoState.value = createMediaUploadState(); form.clearErrors(); } @@ -71,7 +59,7 @@ function populateForm(expense: ExpenseListItem | null | undefined) { form.amount = String(expense.amount); form.description = expense.description; - existingPhotoId.value = expense.photos?.[0]?.id ?? null; + photoState.value = createMediaUploadState(expense.photos ?? []); } useFormDialog({ @@ -91,13 +79,7 @@ function buildFormData(forUpdate: boolean): FormData { formData.append('amount', parseRupiah(form.amount)); formData.append('description', form.description); - const removeMediaIds = [...form.remove_media_ids]; - - if (form.photos.length > 0 && existingPhotoId.value !== null) { - removeMediaIds.push(existingPhotoId.value); - } - - appendPhotosToFormData(formData, form.photos, removeMediaIds); + appendRootPhotosToFormData(formData, photoState.value); return formData; } @@ -148,8 +130,8 @@ function submit() { :maxlength="FIELD_LIMITS.description" /> - + diff --git a/resources/js/pages/admin/manage/purchases/form/PurchasePosForm.vue b/resources/js/pages/admin/manage/purchases/form/PurchasePosForm.vue index aa088ad..529d2a4 100644 --- a/resources/js/pages/admin/manage/purchases/form/PurchasePosForm.vue +++ b/resources/js/pages/admin/manage/purchases/form/PurchasePosForm.vue @@ -3,11 +3,9 @@ import { useForm } from '@inertiajs/vue3'; import { Minus, Plus, Save, Search, ShoppingCart, Trash2 } from '@lucide/vue'; import { computed, ref, watch } from 'vue'; import { toast } from 'vue-sonner'; -import PosCatalogCard from '../../shared/PosCatalogCard.vue'; -import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue'; import { DecimalInput } from '@/components/form/decimal-input'; -import { ImageUploadField } from '@/components/form/image-upload-field'; import { RupiahInput } from '@/components/form/rupiah-input'; +import MediaDropzone from '@/components/media/MediaDropzone.vue'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -46,9 +44,11 @@ import { getFirstCoverImage } from '@/lib/catalog-cover'; import { FIELD_LIMITS } from '@/lib/field-limits'; import { formErrors } from '@/lib/form'; import { formatRupiah, parseRupiah } from '@/lib/rupiah'; -import { appendPhotosToFormData } from '@/types/media'; -import type { MediaItem } from '@/types/media'; +import { appendRootPhotosToFormData, createMediaUploadState } from '@/types/media'; +import type { MediaItem, MediaUploadState } from '@/types/media'; import type { PurchaseCartItem, PurchaseCatalogItem, SelectOption } from '@/types/purchase'; +import PosCatalogCard from '../../shared/PosCatalogCard.vue'; +import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue'; const props = defineProps<{ suppliers: SelectOption[]; catalog: PurchaseCatalogItem[]; @@ -70,24 +70,13 @@ const isCreateMode = computed(() => props.method === 'post'); const search = ref(''); const cart = ref([]); const cartDetailOpen = ref(false); -const existingPhotoId = ref(null); - -const currentPhotoUrl = computed(() => props.initialData?.photos?.url ?? null); +const photoState = ref(createMediaUploadState()); const form = useForm({ supplier_id: '', discount: '', shipping_cost: '', notes: '', - photos: [] as File[], - remove_media_ids: [] as number[], -}); - -const photoFile = computed({ - get: () => form.photos[0] ?? null, - set: (file) => { - form.photos = file ? [file] : []; - }, }); function populateForm() { @@ -99,9 +88,9 @@ function populateForm() { form.discount = props.initialData.discount; form.shipping_cost = props.initialData.shipping_cost ?? '0'; form.notes = props.initialData.notes; - form.photos = []; - form.remove_media_ids = []; - existingPhotoId.value = props.initialData.photos?.id ?? null; + photoState.value = createMediaUploadState( + props.initialData.photos ? [props.initialData.photos] : [], + ); cart.value = props.initialData.items.map((item) => ({ ...item })); } @@ -311,13 +300,7 @@ function buildFormData(): FormData { }); } - const removeMediaIds = [...form.remove_media_ids]; - - if (form.photos.length > 0 && existingPhotoId.value !== null) { - removeMediaIds.push(existingPhotoId.value); - } - - appendPhotosToFormData(formData, form.photos, removeMediaIds); + appendRootPhotosToFormData(formData, photoState.value); return formData; } @@ -382,12 +365,10 @@ function submit() { Belum ada varian

+ ]" @click="!getCartItem(price.id) && addToCart(rawMaterial, price)">

@@ -427,12 +408,9 @@ function submit() { Ringkasan Belanja - @@ -551,8 +529,8 @@ function submit() { - +