|
@@ -57,7 +57,7 @@
|
|
|
:searchQuery="searchQuery"
|
|
:searchQuery="searchQuery"
|
|
|
@update-status="handleUpdateStatus"
|
|
@update-status="handleUpdateStatus"
|
|
|
@delete-order="handleDeleteOrder"
|
|
@delete-order="handleDeleteOrder"
|
|
|
- @attach-file="handleAttachFile"
|
|
|
|
|
|
|
+ @attach-file="handleAttachFiles"
|
|
|
@upload-photo="handleUploadPhoto"
|
|
@upload-photo="handleUploadPhoto"
|
|
|
@delete-file="handleDeleteFile"
|
|
@delete-file="handleDeleteFile"
|
|
|
@delete-photo="handleDeletePhoto"
|
|
@delete-photo="handleDeletePhoto"
|
|
@@ -210,6 +210,50 @@
|
|
|
<textarea v-model="orderForm.review_text" class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-3 text-xs h-20 italic" />
|
|
<textarea v-model="orderForm.review_text" class="w-full bg-background/50 border border-border/50 rounded-xl px-4 py-3 text-xs h-20 italic" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- Management Sections (Files & Photos) -->
|
|
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
|
+ <!-- Files Section -->
|
|
|
|
|
+ <div class="p-4 bg-muted/30 rounded-2xl border border-border/20 space-y-4">
|
|
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
|
|
+ <span class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">{{ t("admin.fields.sourceFiles") }}</span>
|
|
|
|
|
+ <label class="p-1.5 bg-blue-500/10 text-blue-500 rounded-lg cursor-pointer hover:bg-blue-500 hover:text-white transition-all shadow-sm">
|
|
|
|
|
+ <Plus class="w-3.5 h-3.5" />
|
|
|
|
|
+ <input type="file" class="hidden" multiple @change="e => handleAttachFiles((e.target as HTMLInputElement).files)" />
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="space-y-2 max-h-[200px] overflow-y-auto pr-1 custom-scrollbar">
|
|
|
|
|
+ <div v-for="f in (editingOrder?.files || [])" :key="f.id" class="flex items-center justify-between p-2 bg-background/50 rounded-xl border border-border/50 text-[11px]">
|
|
|
|
|
+ <span class="truncate max-w-[150px] font-medium">{{ f.filename }}</span>
|
|
|
|
|
+ <div class="flex gap-1">
|
|
|
|
|
+ <a :href="`${RESOURCES_BASE_URL}/${f.file_path}`" target="_blank" class="p-1.5 hover:bg-primary/10 rounded-md text-primary transition-colors"><Database class="w-3 h-3" /></a>
|
|
|
|
|
+ <button type="button" @click="handleDeleteFile(editingOrder.id, f.id, f.filename)" class="p-1.5 hover:bg-rose-500/10 rounded-md text-rose-500 transition-colors"><Trash2 class="w-3 h-3" /></button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p v-if="!editingOrder?.files?.length" class="text-[10px] text-muted-foreground italic text-center py-4">No files attached</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Photos Section -->
|
|
|
|
|
+ <div class="p-4 bg-muted/30 rounded-2xl border border-border/20 space-y-4">
|
|
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
|
|
+ <span class="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">{{ t("admin.fields.photoReport") }}</span>
|
|
|
|
|
+ <label class="p-1.5 bg-emerald-500/10 text-emerald-500 rounded-lg cursor-pointer hover:bg-emerald-500 hover:text-white transition-all shadow-sm">
|
|
|
|
|
+ <Plus class="w-3.5 h-3.5" />
|
|
|
|
|
+ <input type="file" class="hidden" accept="image/*" @change="e => handleUploadPhoto(editingOrder.id, (e.target as HTMLInputElement).files?.[0])" />
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex flex-wrap gap-2 max-h-[200px] overflow-y-auto pr-1">
|
|
|
|
|
+ <div v-for="p in (editingOrder?.photos || [])" :key="p.id" class="relative group">
|
|
|
|
|
+ <img :src="`${RESOURCES_BASE_URL}/${p.file_path}`" class="w-12 h-12 object-cover rounded-lg border border-border/50 shadow-sm" />
|
|
|
|
|
+ <button type="button" @click="handleDeletePhoto(editingOrder.id, p.id)" class="absolute -top-1 -right-1 p-0.5 bg-rose-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-lg">
|
|
|
|
|
+ <X class="w-2.5 h-2.5" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p v-if="!editingOrder?.photos?.length" class="text-[10px] text-muted-foreground italic text-center py-4 w-full">No photos uploaded</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<div class="flex gap-4 pt-4">
|
|
<div class="flex gap-4 pt-4">
|
|
|
<Button type="button" variant="hero" class="flex-1 rounded-2xl h-12 bg-muted hover:bg-muted/80 text-foreground" @click="showOrderEditModal = false">{{ t("admin.actions.cancel") }}</Button>
|
|
<Button type="button" variant="hero" class="flex-1 rounded-2xl h-12 bg-muted hover:bg-muted/80 text-foreground" @click="showOrderEditModal = false">{{ t("admin.actions.cancel") }}</Button>
|
|
|
<Button type="submit" variant="hero" class="flex-[2] rounded-2xl h-12 shadow-glow">{{ t("admin.actions.save") }}</Button>
|
|
<Button type="submit" variant="hero" class="flex-[2] rounded-2xl h-12 shadow-glow">{{ t("admin.actions.save") }}</Button>
|
|
@@ -517,11 +561,26 @@ const handleDeleteOrder = async (id: number) => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const handleAttachFile = async (id: number, file?: File) => {
|
|
|
|
|
- if (!file) return;
|
|
|
|
|
|
|
+const handleAttachFiles = async (files: FileList | File | null, orderId?: number) => {
|
|
|
|
|
+ if (!files) return;
|
|
|
|
|
+ const id = orderId || editingOrder.value?.id;
|
|
|
|
|
+ if (!id) return;
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
- const fd = new FormData(); fd.append("file", file);
|
|
|
|
|
- await adminAttachFile(id, fd); toast.success("File attached"); fetchData();
|
|
|
|
|
|
|
+ const fileArray = files instanceof FileList ? Array.from(files) : [files];
|
|
|
|
|
+ for (const file of fileArray) {
|
|
|
|
|
+ const fd = new FormData();
|
|
|
|
|
+ fd.append("file", file);
|
|
|
|
|
+ await adminAttachFile(id, fd);
|
|
|
|
|
+ }
|
|
|
|
|
+ toast.success(`${fileArray.length} file(s) attached`);
|
|
|
|
|
+ await fetchData();
|
|
|
|
|
+
|
|
|
|
|
+ // Re-sync editingOrder if open
|
|
|
|
|
+ if (editingOrder.value?.id === id) {
|
|
|
|
|
+ const updatedOrder = orders.value.find(o => o.id === id);
|
|
|
|
|
+ if (updatedOrder) editingOrder.value = updatedOrder;
|
|
|
|
|
+ }
|
|
|
} catch (err: any) { toast.error(err.message); }
|
|
} catch (err: any) { toast.error(err.message); }
|
|
|
};
|
|
};
|
|
|
|
|
|