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() {
-
+