|
|
@@ -0,0 +1,235 @@
|
|
|
+<template>
|
|
|
+ <div class="space-y-6">
|
|
|
+ <!-- Action Bar -->
|
|
|
+ <div class="flex justify-between items-center bg-card/30 p-4 rounded-2xl border border-border/50">
|
|
|
+ <div class="flex items-center gap-3">
|
|
|
+ <Package class="w-5 h-5 text-primary" />
|
|
|
+ <h2 class="text-sm font-black uppercase tracking-widest">{{ t("admin.tabs.warehouse") }}</h2>
|
|
|
+ </div>
|
|
|
+ <button @click="showAddModal = true" class="flex items-center gap-2 bg-primary text-white px-4 py-2 rounded-xl text-xs font-bold hover:shadow-glow transition-all">
|
|
|
+ <Plus class="w-4 h-4" />
|
|
|
+ {{ t("admin.actions.add") }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Stock Table -->
|
|
|
+ <div v-if="stock.length === 0" class="flex flex-col items-center justify-center py-20 bg-card/40 border border-border/50 rounded-3xl opacity-50">
|
|
|
+ <PackageOpen class="w-12 h-12 text-muted-foreground/20 mb-4" />
|
|
|
+ <p class="text-sm text-muted-foreground">{{ t("admin.labels.noStockFound") }}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else class="bg-card/30 border border-border/50 rounded-3xl overflow-hidden shadow-xl">
|
|
|
+ <div class="overflow-x-auto">
|
|
|
+ <table class="w-full text-left border-collapse">
|
|
|
+ <thead>
|
|
|
+ <tr class="bg-muted/30 border-b border-border/50 text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
|
|
|
+ <th class="p-4">{{ t("admin.fields.material") }}</th>
|
|
|
+ <th class="p-4">{{ t("admin.fields.colors") }}</th>
|
|
|
+ <th class="p-4 text-center">{{ t("admin.fields.quantity") }}</th>
|
|
|
+ <th class="p-4 text-center">{{ t("admin.fields.status") }}</th>
|
|
|
+ <th class="p-4 text-right">{{ t("admin.actions.actions") }}</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody class="divide-y divide-border/20">
|
|
|
+ <tr v-for="item in stock" :key="item.id" class="hover:bg-white/5 transition-colors group">
|
|
|
+ <td class="p-4">
|
|
|
+ <div class="flex flex-col">
|
|
|
+ <span class="text-sm font-bold">{{ item.material_name_en }}</span>
|
|
|
+ <span class="text-[10px] text-muted-foreground font-mono opacity-50">#ID {{ item.material_id }}</span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td class="p-4">
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <div class="w-3 h-3 rounded-full border border-border/50" :style="{ backgroundColor: item.color_name.toLowerCase() }"></div>
|
|
|
+ <span class="text-xs font-semibold">{{ item.color_name }}</span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td class="p-4 text-center">
|
|
|
+ <span class="text-sm font-mono font-bold" :class="item.quantity <= 0 ? 'text-rose-500' : 'text-emerald-500'">
|
|
|
+ {{ item.quantity }}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td class="p-4 text-center">
|
|
|
+ <button @click="toggleStatus(item)"
|
|
|
+ :class="['px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border transition-all',
|
|
|
+ item.is_active ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20 hover:bg-emerald-500/20' : 'bg-rose-500/10 text-rose-500 border-rose-500/20 hover:bg-rose-500/20'
|
|
|
+ ]">
|
|
|
+ {{ item.is_active ? t('admin.labels.current') : t('admin.labels.noFiles') /* repurposed noFiles as inactive for now */ }}
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+ <td class="p-4 text-right">
|
|
|
+ <div class="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
|
+ <button @click="handleEdit(item)" class="p-2 hover:bg-primary/10 rounded-lg text-primary transition-colors"><Edit2 class="w-4 h-4" /></button>
|
|
|
+ <button @click="handleDelete(item.id)" class="p-2 hover:bg-rose-500/10 rounded-lg text-rose-500 transition-colors"><Trash2 class="w-4 h-4" /></button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Pagination (Placeholder) -->
|
|
|
+ <div v-if="total > 50" class="flex items-center justify-center gap-2 py-4">
|
|
|
+ <button v-for="p in Math.ceil(total / 50)" :key="p"
|
|
|
+ @click="$emit('update-page', p)"
|
|
|
+ :class="['w-8 h-8 rounded-lg font-bold text-xs transition-all', currentPage === p ? 'bg-primary text-white shadow-glow' : 'bg-card border border-border/50 text-muted-foreground hover:border-primary/50']">
|
|
|
+ {{ p }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Add/Edit Modal -->
|
|
|
+ <div v-if="showAddModal" class="fixed inset-0 z-[10000] flex items-center justify-center p-4">
|
|
|
+ <div class="absolute inset-0 bg-background/90 backdrop-blur-md" @click="closeModal" />
|
|
|
+ <div class="relative w-full max-w-md bg-card border border-primary/20 rounded-3xl p-8 shadow-2xl">
|
|
|
+ <h3 class="text-xl font-black font-display text-gradient mb-6">
|
|
|
+ {{ editingId ? t('admin.modals.editMaterial') : t('admin.actions.add') }}
|
|
|
+ </h3>
|
|
|
+
|
|
|
+ <form @submit.prevent="handleSubmit" class="space-y-4">
|
|
|
+ <div class="space-y-1">
|
|
|
+ <label class="text-[10px] font-bold uppercase text-muted-foreground ml-1">{{ t('admin.fields.material') }}</label>
|
|
|
+ <select v-model="form.material_id" required class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm focus:ring-2 ring-primary/20 outline-none">
|
|
|
+ <option v-for="m in materials" :key="m.id" :value="m.id">{{ m.name_en }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="space-y-1">
|
|
|
+ <label class="text-[10px] font-bold uppercase text-muted-foreground ml-1">{{ t('admin.fields.colors') }}</label>
|
|
|
+ <input v-model="form.color_name" required placeholder="Ex: Black, Emerald, Red..." class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm focus:ring-2 ring-primary/20 outline-none" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="grid grid-cols-2 gap-4">
|
|
|
+ <div class="space-y-1">
|
|
|
+ <label class="text-[10px] font-bold uppercase text-muted-foreground ml-1">{{ t('admin.fields.quantity') }}</label>
|
|
|
+ <input v-model.number="form.quantity" type="number" step="0.1" class="w-full bg-background border border-border/50 rounded-xl px-4 py-3 text-sm focus:ring-2 ring-primary/20 outline-none" />
|
|
|
+ </div>
|
|
|
+ <div class="space-y-1 flex flex-col justify-end pb-2">
|
|
|
+ <label class="flex items-center gap-2 cursor-pointer group">
|
|
|
+ <input type="checkbox" v-model="form.is_active" class="hidden" />
|
|
|
+ <div class="w-5 h-5 rounded border border-border/50 flex items-center justify-center group-hover:border-primary transition-colors" :class="form.is_active ? 'bg-primary border-primary' : ''">
|
|
|
+ <Check v-if="form.is_active" class="w-3 h-3 text-white" />
|
|
|
+ </div>
|
|
|
+ <span class="text-xs font-bold uppercase">{{ t('admin.fields.publishImmediately') }}</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex gap-4 pt-4">
|
|
|
+ <button type="button" @click="closeModal" class="flex-1 px-4 py-3 rounded-xl border border-border/50 hover:bg-white/5 transition-colors text-xs font-bold">{{ t('admin.actions.cancel') }}</button>
|
|
|
+ <button type="submit" class="flex-2 bg-primary text-white px-8 py-3 rounded-xl text-xs font-bold hover:shadow-glow transition-all">
|
|
|
+ {{ editingId ? t('admin.actions.save') : t('admin.actions.add') }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, onMounted } from "vue";
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
+import { Package, Plus, PackageOpen, Check, Edit2, Trash2 } from "lucide-vue-next";
|
|
|
+import { toast } from "vue3-toastify";
|
|
|
+import {
|
|
|
+ adminGetWarehouseStock,
|
|
|
+ adminAddWarehouseStock,
|
|
|
+ adminUpdateWarehouseStock,
|
|
|
+ adminDeleteWarehouseStock
|
|
|
+} from "@/lib/api";
|
|
|
+
|
|
|
+const { t } = useI18n();
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ materials: any[];
|
|
|
+}>();
|
|
|
+
|
|
|
+const stock = ref<any[]>([]);
|
|
|
+const total = ref(0);
|
|
|
+const currentPage = ref(1);
|
|
|
+const showAddModal = ref(false);
|
|
|
+const editingId = ref<number | null>(null);
|
|
|
+
|
|
|
+const form = reactive({
|
|
|
+ material_id: 0,
|
|
|
+ color_name: "",
|
|
|
+ quantity: 1,
|
|
|
+ is_active: true,
|
|
|
+ notes: ""
|
|
|
+});
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchStock();
|
|
|
+ if (props.materials.length > 0) {
|
|
|
+ form.material_id = props.materials[0].id;
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+async function fetchStock() {
|
|
|
+ try {
|
|
|
+ const res = await adminGetWarehouseStock(currentPage.value);
|
|
|
+ stock.value = res.stock;
|
|
|
+ total.value = res.total;
|
|
|
+ } catch (err: any) {
|
|
|
+ toast.error(t('admin.toasts.loadError'));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function toggleStatus(item: any) {
|
|
|
+ try {
|
|
|
+ await adminUpdateWarehouseStock(item.id, { is_active: !item.is_active });
|
|
|
+ item.is_active = !item.is_active;
|
|
|
+ toast.success(t('admin.toasts.statusUpdated'));
|
|
|
+ } catch (err: any) {
|
|
|
+ toast.error(err.message);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleEdit(item: any) {
|
|
|
+ editingId.value = item.id;
|
|
|
+ Object.assign(form, {
|
|
|
+ material_id: item.material_id,
|
|
|
+ color_name: item.color_name,
|
|
|
+ quantity: item.quantity,
|
|
|
+ is_active: item.is_active,
|
|
|
+ notes: item.notes || ""
|
|
|
+ });
|
|
|
+ showAddModal.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+async function handleDelete(id: number) {
|
|
|
+ if (!confirm(t('admin.questions.deletePhoto'))) return; // Repurposing confirm
|
|
|
+ try {
|
|
|
+ await adminDeleteWarehouseStock(id);
|
|
|
+ toast.success(t('admin.toasts.materialDeleted'));
|
|
|
+ fetchStock();
|
|
|
+ } catch (err: any) {
|
|
|
+ toast.error(err.message);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function closeModal() {
|
|
|
+ showAddModal.value = false;
|
|
|
+ editingId.value = null;
|
|
|
+ form.color_name = "";
|
|
|
+ form.quantity = 1;
|
|
|
+ form.is_active = true;
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSubmit() {
|
|
|
+ try {
|
|
|
+ if (editingId.value) {
|
|
|
+ await adminUpdateWarehouseStock(editingId.value, form);
|
|
|
+ toast.success(t('admin.toasts.materialSaved'));
|
|
|
+ } else {
|
|
|
+ await adminAddWarehouseStock(form);
|
|
|
+ toast.success(t('admin.toasts.materialSaved'));
|
|
|
+ }
|
|
|
+ closeModal();
|
|
|
+ fetchStock();
|
|
|
+ } catch (err: any) {
|
|
|
+ toast.error(err.message);
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|