Files
mbmanager-dashboard/src/views/pages/WarehouseItems/ListWarehouseItems.vue
T
2026-08-19 06:02:20 +03:30

547 lines
19 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { onMounted, ref, watch } from 'vue';
import { useToast } from 'primevue/usetoast';
import { useRouter, useRoute } from 'vue-router';
import { useConfirm } from 'primevue/useconfirm';
import ConfirmPopup from 'primevue/confirmpopup';
import { WarehouseItemService } from '@/service/WarehouseItemService';
const toast = useToast();
const router = useRouter();
const route = useRoute();
const confirm = useConfirm();
const items = ref([]);
const totalCount = ref(0);
const page = ref(Number(route.query.page) || 1);
const perPage = ref(Number(route.query.per_page) || 20);
const searchPhrase = ref(route.query.search || '');
const loading = ref(true);
// Create/Edit dialog
const showDialog = ref(false);
const editMode = ref(false);
const submitting = ref(false);
const selectedItem = ref(null);
const form = ref({
nameFa: '',
nameEn: '',
code: '',
unitFa: '',
unitEn: '',
category: '',
description: ''
});
// ─── Common units ─────────────────────────────────────────────────────────────
const UNITS = [
{ fa: 'عدد', en: 'Piece' },
{ fa: 'تن', en: 'Ton' },
{ fa: 'کیلوگرم', en: 'Kilogram' },
{ fa: 'متر', en: 'Meter' },
{ fa: 'متر مربع', en: 'Square Meter' },
{ fa: 'متر مکعب', en: 'Cubic Meter' },
{ fa: 'لیتر', en: 'Liter' },
{ fa: 'بسته', en: 'Package' },
{ fa: 'رول', en: 'Roll' },
{ fa: 'جعبه', en: 'Box' },
{ fa: 'کیسه', en: 'Bag' },
{ fa: 'پالت', en: 'Pallet' },
];
let searchTimeout = null;
onMounted(() => {
loadData();
});
watch(searchPhrase, (val) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
router.push({ query: { ...route.query, search: val || undefined, page: 1 } });
}, 400);
});
watch(() => route.query, () => {
page.value = Number(route.query.page) || 1;
perPage.value = Number(route.query.per_page) || 20;
searchPhrase.value = route.query.search || '';
loadData();
}, { deep: true });
const loadData = async () => {
loading.value = true;
try {
const result = await WarehouseItemService.listItems({
page: page.value,
limit: perPage.value,
search: searchPhrase.value || undefined
});
items.value = result.items || [];
totalCount.value = result.total || 0;
} catch (e) {
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در بارگذاری اطلاعات', life: 3000 });
} finally {
loading.value = false;
}
};
const onPageChange = (event) => {
router.push({ query: { ...route.query, page: event.page + 1, per_page: event.rows } });
};
const openCreateDialog = () => {
editMode.value = false;
selectedItem.value = null;
form.value = { nameFa: '', nameEn: '', code: '', unitFa: '', unitEn: '', category: '', description: '' };
showDialog.value = true;
};
const openEditDialog = (item) => {
editMode.value = true;
selectedItem.value = item;
form.value = {
nameFa: item.nameFa,
nameEn: item.nameEn,
code: item.code || '',
unitFa: item.unitFa,
unitEn: item.unitEn,
category: item.category || '',
description: item.description || ''
};
showDialog.value = true;
};
const onUnitSelect = (unit) => {
form.value.unitFa = unit.fa;
form.value.unitEn = unit.en;
};
const submitForm = async () => {
if (!form.value.nameFa || !form.value.nameEn || !form.value.unitFa || !form.value.unitEn) {
toast.add({ severity: 'warn', summary: 'توجه', detail: 'لطفاً تمام فیلدهای ضروری را پر کنید', life: 3000 });
return;
}
submitting.value = true;
try {
if (editMode.value) {
await WarehouseItemService.updateItem(selectedItem.value._id, form.value);
toast.add({ severity: 'success', summary: 'موفق', detail: 'آیتم با موفقیت ویرایش شد', life: 3000 });
} else {
await WarehouseItemService.createItem(form.value);
toast.add({ severity: 'success', summary: 'موفق', detail: 'آیتم با موفقیت ایجاد شد', life: 3000 });
}
showDialog.value = false;
loadData();
} catch (e) {
toast.add({ severity: 'error', summary: 'خطا', detail: e.response?.data?.message || 'عملیات ناموفق بود', life: 4000 });
} finally {
submitting.value = false;
}
};
const confirmDelete = (item, event) => {
confirm.require({
target: event.currentTarget,
message: `آیا از غیرفعال‌سازی "${item.nameFa}" اطمینان دارید؟`,
icon: 'pi pi-exclamation-triangle',
rejectProps: { label: 'خیر', severity: 'secondary', outlined: true },
acceptProps: { label: 'بله', severity: 'danger' },
accept: async () => {
try {
await WarehouseItemService.deleteItem(item._id);
toast.add({ severity: 'success', summary: 'موفق', detail: 'آیتم غیرفعال شد', life: 3000 });
loadData();
} catch (e) {
toast.add({ severity: 'error', summary: 'خطا', detail: 'عملیات ناموفق بود', life: 3000 });
}
}
});
};
const viewStock = (itemId) => {
router.push(`/warehouse-items/stock/${itemId}`);
};
</script>
<template>
<ConfirmPopup />
<div class="warehouse-items-page">
<!-- ─── Header ─────────────────────────────────────────────────── -->
<div class="page-header">
<div class="header-content">
<div class="header-icon">
<i class="pi pi-box"></i>
</div>
<div>
<h1 class="page-title">اقلام انبار</h1>
<p class="page-subtitle">مدیریت محصولات و مصالح انبار</p>
</div>
</div>
<div class="header-actions">
<Button
label="افزودن قلم جدید"
icon="pi pi-plus"
class="create-btn"
@click="openCreateDialog"
/>
</div>
</div>
<!-- ─── Main card ───────────────────────────────────────────────── -->
<div class="card main-card">
<DataTable
:value="items"
:loading="loading"
dataKey="_id"
:paginator="true"
:rows="perPage"
:total-records="totalCount"
lazy
@page="onPageChange($event)"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
:rowsPerPageOptions="[20, 40, 80]"
currentPageReportTemplate="نمایش {first} تا {last} از {totalRecords} قلم"
stripedRows
responsiveLayout="scroll"
>
<template #header>
<div class="table-header">
<span class="table-title">لیست اقلام</span>
<IconField>
<InputIcon><i class="pi pi-search" /></InputIcon>
<InputText v-model="searchPhrase" placeholder="جستجو..." class="search-input" />
</IconField>
</div>
</template>
<template #loading>
<div class="flex flex-col items-center justify-center p-8 gap-3">
<ProgressSpinner style="width: 42px; height: 42px" strokeWidth="4" />
<span class="text-sm font-medium text-surface-500">در حال بارگذاری اقلام انبار...</span>
</div>
</template>
<template #empty>
<div v-if="!loading" class="empty-state">
<i class="pi pi-inbox empty-icon"></i>
<p>هیچ قلمی یافت نشد</p>
<Button label="افزودن اولین قلم" icon="pi pi-plus" @click="openCreateDialog" />
</div>
</template>
<Column field="code" header="کد" style="width:8%">
<template #body="{ data }">
<span class="code-badge" v-if="data.code">{{ data.code }}</span>
<span class="text-muted" v-else>—</span>
</template>
</Column>
<Column field="nameFa" header="نام فارسی" sortable style="width:18%">
<template #body="{ data }">
<div class="item-name">
<span class="name-fa">{{ data.nameFa }}</span>
</div>
</template>
</Column>
<Column field="nameEn" header="نام انگلیسی" sortable style="width:18%">
<template #body="{ data }">
<span class="name-en">{{ data.nameEn }}</span>
</template>
</Column>
<Column header="واحد" style="width:12%">
<template #body="{ data }">
<span class="unit-badge">{{ data.unitFa }} / {{ data.unitEn }}</span>
</template>
</Column>
<Column field="category" header="دسته‌بندی" style="width:14%">
<template #body="{ data }">
<span v-if="data.category" class="category-chip">{{ data.category }}</span>
<span v-else class="text-muted">—</span>
</template>
</Column>
<Column field="isActive" header="وضعیت" style="width:9%">
<template #body="{ data }">
<Tag :severity="data.isActive ? 'success' : 'danger'"
:value="data.isActive ? 'فعال' : 'غیرفعال'" />
</template>
</Column>
<Column header="عملیات" style="width:21%">
<template #body="{ data }">
<div class="action-buttons">
<Button
icon="pi pi-chart-bar"
outlined
rounded
severity="info"
v-tooltip.top="'مشاهده موجودی'"
@click="viewStock(data._id)"
/>
<Button
icon="pi pi-pencil"
outlined
rounded
v-tooltip.top="'ویرایش'"
@click="openEditDialog(data)"
/>
<Button
icon="pi pi-ban"
outlined
rounded
severity="danger"
v-tooltip.top="'غیرفعال‌سازی'"
@click="confirmDelete(data, $event)"
/>
</div>
</template>
</Column>
</DataTable>
</div>
<!-- ─── Create/Edit Dialog ──────────────────────────────────────── -->
<Dialog
v-model:visible="showDialog"
:header="editMode ? 'ویرایش قلم انبار' : 'افزودن قلم جدید'"
:style="{ width: '580px' }"
modal
:dir="'rtl'"
>
<div class="form-grid">
<div class="form-row">
<div class="form-field">
<label>نام فارسی <span class="required">*</span></label>
<InputText v-model="form.nameFa" placeholder="مثال: سیمان" class="w-full" />
</div>
<div class="form-field">
<label>نام انگلیسی <span class="required">*</span></label>
<InputText v-model="form.nameEn" placeholder="e.g. Cement" class="w-full" dir="ltr" />
</div>
</div>
<div class="form-row">
<div class="form-field">
<label>کد (اختیاری)</label>
<InputText v-model="form.code" placeholder="مثال: CEM" class="w-full" dir="ltr" />
</div>
<div class="form-field">
<label>دسته‌بندی</label>
<InputText v-model="form.category" placeholder="مثال: مصالح ساختمانی" class="w-full" />
</div>
</div>
<!-- Unit picker -->
<div class="form-field full-width">
<label>واحد شمارش <span class="required">*</span></label>
<div class="unit-picker">
<button
v-for="u in UNITS"
:key="u.en"
class="unit-chip"
:class="{ active: form.unitFa === u.fa }"
type="button"
@click="onUnitSelect(u)"
>
{{ u.fa }}
<span class="unit-en">{{ u.en }}</span>
</button>
</div>
<div class="unit-custom-row">
<InputText v-model="form.unitFa" placeholder="واحد فارسی سفارشی" class="w-1/2" />
<InputText v-model="form.unitEn" placeholder="Custom unit (EN)" class="w-1/2" dir="ltr" />
</div>
</div>
<div class="form-field full-width">
<label>توضیحات</label>
<Textarea v-model="form.description" rows="2" class="w-full" placeholder="توضیحات اختیاری..." autoResize />
</div>
</div>
<template #footer>
<Button label="انصراف" icon="pi pi-times" text @click="showDialog = false" />
<Button
:label="editMode ? 'ذخیره تغییرات' : 'ایجاد قلم'"
icon="pi pi-check"
:loading="submitting"
@click="submitForm"
/>
</template>
</Dialog>
</div>
</template>
<style scoped>
.warehouse-items-page {
padding: 1.5rem;
direction: rtl;
}
/* Header */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
}
.header-content {
display: flex;
align-items: center;
gap: 1rem;
}
.header-icon {
width: 52px;
height: 52px;
border-radius: 14px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: #fff;
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.35);
}
.page-title {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
}
.page-subtitle {
margin: 0;
color: var(--text-color-secondary);
font-size: 0.875rem;
}
.create-btn {
background: linear-gradient(135deg, #6366f1, #8b5cf6) !important;
border: none !important;
padding: 0.65rem 1.5rem !important;
font-weight: 600 !important;
}
/* Card */
.main-card {
border-radius: 16px;
overflow: hidden;
}
/* Table header */
.table-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0;
}
.table-title {
font-size: 1rem;
font-weight: 600;
}
.search-input { width: 260px; }
/* Empty state */
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-color-secondary);
}
.empty-icon {
font-size: 3rem;
display: block;
margin-bottom: 1rem;
opacity: 0.4;
}
/* Badges / chips */
.code-badge {
font-family: monospace;
background: var(--surface-100);
padding: 2px 8px;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 600;
}
.name-fa { font-weight: 600; }
.name-en {
color: var(--text-color-secondary);
font-size: 0.875rem;
direction: ltr;
display: inline-block;
}
.unit-badge {
background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(139,92,246,0.1));
color: #6366f1;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
}
.category-chip {
background: var(--surface-200);
padding: 3px 10px;
border-radius: 20px;
font-size: 0.8rem;
}
.text-muted { color: var(--text-color-secondary); opacity: 0.5; }
/* Actions */
.action-buttons {
display: flex;
gap: 0.4rem;
align-items: center;
}
/* Form */
.form-grid { display: flex; flex-direction: column; gap: 1rem; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.form-field { display: flex; flex-direction: column; gap: 0.4rem; }
.form-field.full-width { grid-column: span 2; }
.form-field label { font-weight: 600; font-size: 0.9rem; }
.required { color: #ef4444; }
/* Unit picker */
.unit-picker {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.unit-chip {
border: 1.5px solid var(--surface-border);
background: var(--surface-50);
border-radius: 20px;
padding: 5px 14px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.4rem;
transition: all 0.2s;
color: var(--text-color);
}
.unit-chip:hover { border-color: #6366f1; color: #6366f1; background: rgba(99,102,241,0.06); }
.unit-chip.active {
border-color: #6366f1;
background: linear-gradient(135deg, rgba(99,102,241,0.15), rgba(139,92,246,0.15));
color: #6366f1;
}
.unit-en {
font-size: 0.75rem;
opacity: 0.7;
direction: ltr;
display: inline-block;
}
.unit-custom-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
</style>