Files
nademitex_admin/src/views/catalogs/CatalogsView.vue
T
2026-09-13 15:39:08 +03:30

398 lines
13 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 lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import AdminPageSkeleton from '@/components/ui/AdminPageSkeleton.vue'
import BaseModal from '@/components/ui/BaseModal.vue'
import BaseSwitch from '@/components/ui/BaseSwitch.vue'
import BaseEmptyState from '@/components/ui/BaseEmptyState.vue'
import ActiveSwitchCell from '@/components/ui/ActiveSwitchCell.vue'
import AdminIcon from '@/components/icons/AdminIcon.vue'
import AdminSelect from '@/components/ui/AdminSelect.vue'
import AdminImagePicker from '@/components/ui/AdminImagePicker.vue'
import { catalogsApi, companiesApi } from '@/services/api'
import { ApiError } from '@/services/http'
import { useToast } from '@/composables/useToast'
import type { Catalog, Company } from '@/types'
const catalogList = ref<Catalog[]>([])
const companies = ref<Company[]>([])
const searchQuery = ref('')
const companyFilter = ref('all')
const loading = ref(true)
const modalMode = ref<'add' | 'edit' | null>(null)
const editingId = ref<string | null>(null)
const formError = ref('')
const saving = ref(false)
const toast = useToast()
const companyFilterOptions = computed(() => [
{ value: 'all', label: 'همه شرکت‌ها' },
...companies.value.map((company) => ({ value: company.id, label: company.name })),
])
const companyFormOptions = computed(() =>
companies.value.map((company) => ({ value: company.id, label: company.name })),
)
const emptyForm = () => ({
companyId: companies.value[0]?.id ?? '',
code: '',
name: '',
displayOrder: catalogList.value.length + 1,
isActive: true,
imageUrl: null as string | null,
})
const form = reactive(emptyForm())
const isModalOpen = computed(() => modalMode.value !== null)
const modalTitle = computed(() => (modalMode.value === 'edit' ? 'ویرایش کالیته' : 'افزودن کالیته'))
const isFormValid = computed(() => form.code.trim() && form.name.trim() && form.companyId)
const hasActiveFilters = computed(
() => searchQuery.value.trim().length > 0 || companyFilter.value !== 'all',
)
const filteredCatalogs = computed(() => {
let list = catalogList.value
if (companyFilter.value !== 'all') {
list = list.filter((c) => c.companyId === companyFilter.value)
}
const q = searchQuery.value.trim()
if (q) {
list = list.filter((c) => c.code.includes(q) || c.name.includes(q) || c.companyName.includes(q))
}
return list
})
async function loadData() {
loading.value = true
try {
const [catalogsData, companiesData] = await Promise.all([
catalogsApi.list(),
companiesApi.list(),
])
catalogList.value = catalogsData
companies.value = companiesData
} catch (err) {
toast.error(err instanceof ApiError ? err.message : 'بارگذاری کالیته‌ها انجام نشد.')
} finally {
loading.value = false
}
}
onMounted(() => {
void loadData()
})
function companyName(companyId: string) {
return companies.value.find((c) => c.id === companyId)?.name ?? '—'
}
function openAddModal() {
modalMode.value = 'add'
editingId.value = null
Object.assign(form, emptyForm())
formError.value = ''
}
function openEditModal(catalog: Catalog) {
modalMode.value = 'edit'
editingId.value = catalog.id
form.companyId = catalog.companyId
form.code = catalog.code
form.name = catalog.name
form.displayOrder = catalog.displayOrder
form.isActive = catalog.isActive
form.imageUrl = catalog.imageUrl ?? null
formError.value = ''
}
function closeModal() {
modalMode.value = null
editingId.value = null
formError.value = ''
}
async function submitCatalog() {
if (!isFormValid.value) {
formError.value = 'کد، نام و شرکت الزامی هستند.'
return
}
saving.value = true
try {
if (modalMode.value === 'edit' && editingId.value) {
const updated = await catalogsApi.update(editingId.value, {
code: form.code.trim(),
name: form.name.trim(),
displayOrder: Number(form.displayOrder) || 1,
isActive: form.isActive,
imageUrl: form.imageUrl,
})
const index = catalogList.value.findIndex((c) => c.id === editingId.value)
if (index !== -1) {
catalogList.value[index] = {
...catalogList.value[index]!,
...updated,
companyName: companyName(form.companyId),
}
}
toast.success('اطلاعات کالیته به‌روزرسانی شد.')
} else {
const created = await catalogsApi.create({
companyId: form.companyId,
code: form.code.trim(),
name: form.name.trim(),
displayOrder: Number(form.displayOrder) || 1,
isActive: form.isActive,
imageUrl: form.imageUrl,
})
catalogList.value.push({
...created,
companyName: created.companyName || companyName(form.companyId),
productCount: created.productCount ?? 0,
})
toast.success('کالیته جدید ثبت شد.')
}
closeModal()
} catch (err) {
toast.error(err instanceof ApiError ? err.message : 'ذخیره کالیته انجام نشد.')
} finally {
saving.value = false
}
}
async function persistActive(id: string, isActive: boolean) {
await catalogsApi.setActive(id, isActive)
}
function clearFilters() {
searchQuery.value = ''
companyFilter.value = 'all'
}
</script>
<template>
<div class="catalogs-page">
<div class="admin-page-header">
<div>
<h1 class="admin-page-header__title">کالیته‌ها</h1>
</div>
<button type="button" class="admin-btn admin-btn--primary" @click="openAddModal">
افزودن کالیته
</button>
</div>
<div class="catalogs-page__filters admin-card">
<div class="catalogs-page__search">
<AdminIcon name="search" :size="18" />
<input
v-model="searchQuery"
type="search"
class="catalogs-page__search-input"
placeholder="جستجو بر اساس کد یا نام..."
/>
</div>
<AdminSelect
v-model="companyFilter"
class="catalogs-page__company-filter"
aria-label="فیلتر شرکت"
:options="companyFilterOptions"
/>
</div>
<AdminPageSkeleton v-if="loading" variant="table" />
<BaseEmptyState
v-else-if="!filteredCatalogs.length"
class="admin-card"
icon="catalogs"
:title="hasActiveFilters ? 'نتیجه‌ای یافت نشد' : 'کالیته‌ای ثبت نشده'"
:description="
hasActiveFilters
? 'عبارت جستجو را تغییر دهید یا فیلترها را پاک کنید.'
: 'برای شروع، اولین کالیته را ثبت کنید.'
"
>
<button v-if="hasActiveFilters" type="button" class="admin-btn admin-btn--outline admin-btn--sm" @click="clearFilters">
پاک کردن فیلترها
</button>
<button v-else type="button" class="admin-btn admin-btn--primary admin-btn--sm" @click="openAddModal">
افزودن کالیته
</button>
</BaseEmptyState>
<div v-else class="catalogs-page__table admin-card desktop-only">
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>تصویر</th>
<th>کد</th>
<th>نام کالیته</th>
<th>شرکت</th>
<th class="numeric">ترتیب</th>
<th class="numeric">تعداد محصول</th>
<th class="status">وضعیت</th>
<th class="actions">عملیات</th>
</tr>
</thead>
<tbody>
<tr v-for="catalog in filteredCatalogs" :key="catalog.id">
<td>
<span
class="catalog-thumb"
:class="{ 'catalog-thumb--empty': !catalog.imageUrl }"
:style="catalog.imageUrl ? { backgroundImage: `url(${catalog.imageUrl})` } : undefined"
/>
</td>
<td>{{ catalog.code }}</td>
<td>{{ catalog.name }}</td>
<td>{{ catalog.companyName }}</td>
<td class="numeric">{{ catalog.displayOrder }}</td>
<td class="numeric">{{ catalog.productCount }}</td>
<td class="status">
<ActiveSwitchCell
:id="catalog.id"
v-model="catalog.isActive"
:entity-name="catalog.name"
entity-type="کالیته"
:persist="persistActive"
/>
</td>
<td class="actions">
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEditModal(catalog)">
ویرایش
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="!loading && filteredCatalogs.length" class="compact-only admin-card-list">
<article v-for="catalog in filteredCatalogs" :key="catalog.id" class="admin-card-item">
<div class="admin-card-item__header">
<span
class="catalog-thumb"
:class="{ 'catalog-thumb--empty': !catalog.imageUrl }"
:style="catalog.imageUrl ? { backgroundImage: `url(${catalog.imageUrl})` } : undefined"
/>
<p class="admin-card-item__title">کالیته {{ catalog.code }} — {{ catalog.name }}</p>
<ActiveSwitchCell
:id="catalog.id"
v-model="catalog.isActive"
:entity-name="catalog.name"
entity-type="کالیته"
:persist="persistActive"
/>
</div>
<div class="admin-card-item__meta">
<span>{{ catalog.companyName }}</span>
<span>{{ catalog.productCount }} محصول</span>
<span>ترتیب: {{ catalog.displayOrder }}</span>
</div>
<div class="admin-card-item__actions">
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEditModal(catalog)">
ویرایش
</button>
</div>
</article>
</div>
<BaseModal :open="isModalOpen" :title="modalTitle" size="lg" @close="closeModal">
<form class="admin-form" @submit.prevent="submitCatalog">
<p v-if="formError" class="admin-form__error" role="alert">{{ formError }}</p>
<div class="admin-form__field">
<label>شرکت <span class="admin-form__required">*</span></label>
<AdminSelect
v-model="form.companyId"
block
aria-label="انتخاب شرکت"
placeholder="انتخاب شرکت"
:options="companyFormOptions"
/>
</div>
<div class="admin-form__row">
<div class="admin-form__field">
<label for="catalog-code">کد کالیته <span class="admin-form__required">*</span></label>
<input id="catalog-code" v-model="form.code" type="text" class="admin-input" placeholder="377" />
</div>
<div class="admin-form__field">
<label for="catalog-name">نام کالیته <span class="admin-form__required">*</span></label>
<input id="catalog-name" v-model="form.name" type="text" class="admin-input" />
</div>
</div>
<div class="admin-form__row">
<div class="admin-form__field">
<label for="catalog-order">ترتیب نمایش</label>
<input id="catalog-order" v-model.number="form.displayOrder" type="number" min="1" class="admin-input" />
</div>
<div class="admin-form__field">
<BaseSwitch v-model="form.isActive" label="وضعیت" />
</div>
</div>
<AdminImagePicker v-model="form.imageUrl" label="تصویر بنر کالیته" />
</form>
<template #footer>
<button type="button" class="admin-btn admin-btn--outline" @click="closeModal">انصراف</button>
<button type="button" class="admin-btn admin-btn--primary" :disabled="!isFormValid || saving" @click="submitCatalog">
{{ modalMode === 'edit' ? 'ذخیره تغییرات' : 'ثبت کالیته' }}
</button>
</template>
</BaseModal>
</div>
</template>
<style scoped>
.catalogs-page__filters {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 12px 14px;
margin-bottom: 16px;
align-items: center;
}
.catalogs-page__search {
flex: 1;
min-width: 200px;
display: flex;
align-items: center;
gap: 10px;
color: var(--color-text-muted);
}
.catalogs-page__search-input {
flex: 1;
border: none;
background: transparent;
font-size: 0.875rem;
outline: none;
}
.catalogs-page__company-filter {
width: min(220px, 100%);
}
.catalogs-page__table {
overflow: hidden;
}
.catalog-thumb {
display: block;
width: 72px;
height: 44px;
border-radius: 8px;
background: #e8eceb center / cover no-repeat;
}
.catalog-thumb--empty {
background: #eef1f0;
}
</style>