| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- <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.warehouse.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.unitMass") }}</th>
- <th class="p-4 text-center">{{ t("admin.fields.unitsCount") }}</th>
- <th class="p-4 text-center">{{ t("admin.fields.quantity") }} (kg)</th>
- <th class="p-4 text-center">{{ t("admin.fields.status") }}</th>
- <th class="p-4 text-right">{{ t("admin.labels.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-xs font-mono">{{ item.unit_mass }}</span>
- </td>
- <td class="p-4 text-center">
- <div class="flex items-center justify-center gap-2">
- <span class="text-xs font-bold">{{ item.units_count }}</span>
- <button v-if="item.units_count > 0" @click="handleDeduct(item)" class="p-1 hover:bg-rose-500/10 rounded-md text-rose-500 transition-colors" title="Deduct 1 unit">
- <Minus class="w-3 h-3" />
- </button>
- </div>
- </td>
- <td class="p-4 text-center">
- <span class="text-sm font-mono font-bold" :class="item.quantity <= 0.1 ? '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/95 backdrop-blur-xl" @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.editStock') : t('admin.modals.addStock') }}
- </h3>
-
- <form v-if="materials && materials.length > 0" @submit.prevent="handleSubmit" class="space-y-4">
- <div class="space-y-1">
- <label class="text-[10px] font-black uppercase text-muted-foreground ml-1 tracking-widest">{{ 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 appearance-none">
- <option value="0" disabled>{{ t('admin.actions.select') || 'Select material...' }}</option>
- <option v-for="m in materials" :key="m.id" :value="m.id">{{ m.name_en || m.name_ru }}</option>
- </select>
- </div>
- <div class="space-y-1">
- <label class="text-[10px] font-black uppercase text-muted-foreground ml-1 tracking-widest">{{ t('admin.fields.color') }}</label>
- <input v-model="form.color_name" required placeholder="Ex: Black, Red, Gold..." 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-black uppercase text-muted-foreground ml-1 tracking-widest">{{ t('admin.fields.unitMass') }}</label>
- <input v-model.number="form.unit_mass" type="number" step="0.001" 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" />
- </div>
- <div class="space-y-1">
- <label class="text-[10px] font-black uppercase text-muted-foreground ml-1 tracking-widest">{{ t('admin.fields.unitsCount') }}</label>
- <input v-model.number="form.units_count" type="number" 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" />
- </div>
- </div>
- <div class="grid grid-cols-2 gap-4">
- <div class="space-y-1">
- <label class="text-[10px] font-black uppercase text-muted-foreground ml-1 tracking-widest">{{ t('admin.fields.quantity') }} (kg)</label>
- <input v-model.number="form.quantity" type="number" step="0.001" 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" />
- </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-[10px] font-black uppercase tracking-wider text-muted-foreground">{{ t('admin.fields.active') || 'Active' }}</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-black uppercase tracking-widest">
- {{ t('admin.actions.cancel') }}
- </button>
- <button type="submit" class="flex-2 bg-primary text-white px-8 py-3 rounded-xl text-xs font-black uppercase tracking-widest hover:shadow-glow transition-all">
- {{ editingId ? t('admin.actions.save') : t('admin.actions.add') }}
- </button>
- </div>
- </form>
- <div v-else class="text-center py-10">
- <PackageOpen class="w-12 h-12 text-muted-foreground/20 mx-auto mb-4" />
- <p class="text-xs text-muted-foreground uppercase font-black tracking-widest mb-6">
- {{ t('admin.warehouse.noMaterials') }}
- </p>
- <button @click="closeModal" class="px-6 py-2 bg-primary text-white rounded-xl text-[10px] font-black uppercase tracking-widest">
- {{ t('admin.actions.cancel') }}
- </button>
- </div>
- </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, Minus } from "lucide-vue-next";
- import { toast } from "vue-sonner";
- 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.0,
- unit_mass: 1.0,
- units_count: 1,
- is_active: true,
- notes: ""
- });
- watch(() => [form.unit_mass, form.units_count], ([m, c]) => {
- if (!editingId.value) {
- form.quantity = Number((m * c).toFixed(3));
- }
- });
- 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 handleDeduct(item: any) {
- if (item.units_count <= 0) return;
-
- const newCount = item.units_count - 1;
- const newTotal = Number((Math.max(0, item.quantity - item.unit_mass)).toFixed(3));
-
- try {
- await adminUpdateWarehouseStock(item.id, {
- units_count: newCount,
- quantity: newTotal
- });
- item.units_count = newCount;
- item.quantity = newTotal;
- toast.success(t('admin.toasts.materialSaved'));
- } catch (err: any) {
- toast.error(err.message);
- }
- }
- 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,
- unit_mass: item.unit_mass,
- units_count: item.units_count,
- is_active: item.is_active,
- notes: item.notes || ""
- });
- showAddModal.value = true;
- }
- async function handleDelete(id: number) {
- if (!confirm(t('admin.questions.deletePhoto'))) return;
- 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>
|