2026-08-29 21:06:30 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onMounted, ref, watch } from 'vue';
|
2026-10-06 15:04:46 +03:30
|
|
|
|
import { matchesSearch } from '@/lib/search';
|
2026-10-05 11:56:49 +03:30
|
|
|
|
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
|
2026-10-05 20:00:21 +03:30
|
|
|
|
import { PAGE_SIZE_OPTIONS, usePageSize } from '@/lib/pageSize';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
|
import { api } from '@/lib/api';
|
|
|
|
|
|
import { formatMoney } from '@/lib/money';
|
2026-09-05 17:53:50 +03:30
|
|
|
|
import {
|
|
|
|
|
|
formatJalaliNumeric,
|
|
|
|
|
|
toFaDigits,
|
|
|
|
|
|
} from '@/lib/jalali';
|
|
|
|
|
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
2026-09-10 01:22:24 +03:30
|
|
|
|
import InvoiceFormView from '@/views/InvoiceFormView.vue';
|
2026-10-18 19:39:59 +03:30
|
|
|
|
import { registerTableColumns, useTableColumns } from '@/lib/tableColumns';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Alert as AAlert,
|
|
|
|
|
|
Avatar as AAvatar,
|
|
|
|
|
|
Button as AButton,
|
|
|
|
|
|
Card as ACard,
|
|
|
|
|
|
Col as ACol,
|
|
|
|
|
|
Drawer as ADrawer,
|
|
|
|
|
|
Dropdown as ADropdown,
|
|
|
|
|
|
Empty as AEmpty,
|
|
|
|
|
|
Flex as AFlex,
|
|
|
|
|
|
FormItem as AFormItem,
|
|
|
|
|
|
Input as AInput,
|
|
|
|
|
|
InputNumber as AInputNumber,
|
|
|
|
|
|
Menu as AMenu,
|
|
|
|
|
|
Modal as AModal,
|
|
|
|
|
|
Pagination as APagination,
|
|
|
|
|
|
Row as ARow,
|
|
|
|
|
|
Select as ASelect,
|
|
|
|
|
|
Space as ASpace,
|
|
|
|
|
|
Spin as ASpin,
|
|
|
|
|
|
Table as ATable,
|
|
|
|
|
|
Tag as ATag,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
} from 'ant-design-vue';
|
|
|
|
|
|
import type { TableColumnsType } from 'ant-design-vue';
|
|
|
|
|
|
import {
|
|
|
|
|
|
DownOutlined,
|
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
|
FilterOutlined,
|
|
|
|
|
|
MoreOutlined,
|
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
|
} from '@ant-design/icons-vue';
|
2026-09-05 17:53:50 +03:30
|
|
|
|
import type {
|
|
|
|
|
|
InvoiceDto,
|
|
|
|
|
|
PaginatedResponse,
|
|
|
|
|
|
PersonDto,
|
|
|
|
|
|
QuoteDto,
|
|
|
|
|
|
} from '@mento/shared';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
|
|
|
|
|
const AMenuItem = AMenu.Item;
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
type DocTab = 'all' | 'invoice' | 'quote' | 'credit';
|
|
|
|
|
|
type PayFilter = 'all' | 'paid' | 'unpaid' | 'pending' | 'expired';
|
|
|
|
|
|
|
|
|
|
|
|
type BillingRow = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
entityId: string;
|
|
|
|
|
|
personId: string | null;
|
|
|
|
|
|
companyId: string | null;
|
|
|
|
|
|
doc: 'invoice' | 'quote' | 'credit';
|
|
|
|
|
|
number: string;
|
|
|
|
|
|
date: string;
|
|
|
|
|
|
dueOrValid: string | null;
|
|
|
|
|
|
customerName: string;
|
|
|
|
|
|
personName: string;
|
|
|
|
|
|
amount: number;
|
|
|
|
|
|
currency: string;
|
|
|
|
|
|
statusKey: PayFilter | 'quote';
|
|
|
|
|
|
statusLabel: string;
|
|
|
|
|
|
href: { name: string; params: { id: string } };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
const INVOICE_COL_LABELS = [
|
|
|
|
|
|
'شماره فاکتور',
|
|
|
|
|
|
'تاریخ',
|
|
|
|
|
|
'مشتری',
|
|
|
|
|
|
'نوع',
|
|
|
|
|
|
'وضعیت',
|
|
|
|
|
|
'مبلغ کل',
|
|
|
|
|
|
'ارز',
|
|
|
|
|
|
'مهلت پرداخت',
|
|
|
|
|
|
'عملیات',
|
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
|
|
registerTableColumns('invoices', [...INVOICE_COL_LABELS]);
|
|
|
|
|
|
const tableCols = useTableColumns(() => 'invoices');
|
|
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const route = useRoute();
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const error = ref('');
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const invoices = ref<InvoiceDto[]>([]);
|
|
|
|
|
|
const quotes = ref<QuoteDto[]>([]);
|
|
|
|
|
|
const people = ref<PersonDto[]>([]);
|
2026-08-29 21:06:30 +03:30
|
|
|
|
const q = ref(typeof route.query.q === 'string' ? route.query.q : '');
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const docTab = ref<DocTab>(
|
|
|
|
|
|
route.query.doc === 'quote'
|
|
|
|
|
|
? 'quote'
|
|
|
|
|
|
: route.query.doc === 'credit'
|
|
|
|
|
|
? 'credit'
|
|
|
|
|
|
: route.query.doc === 'invoice'
|
|
|
|
|
|
? 'invoice'
|
|
|
|
|
|
: 'all',
|
2026-08-29 21:06:30 +03:30
|
|
|
|
);
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const payFilter = ref<PayFilter>('all');
|
|
|
|
|
|
const personId = ref(
|
|
|
|
|
|
typeof route.query.personId === 'string' ? route.query.personId : '',
|
2026-09-05 02:29:41 +03:30
|
|
|
|
);
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const from = ref(typeof route.query.from === 'string' ? route.query.from : '');
|
|
|
|
|
|
const to = ref(typeof route.query.to === 'string' ? route.query.to : '');
|
|
|
|
|
|
const amountMin = ref('');
|
|
|
|
|
|
const amountMax = ref('');
|
|
|
|
|
|
const page = ref(1);
|
2026-10-05 20:00:21 +03:30
|
|
|
|
const pageSize = usePageSize('invoices', 10);
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const selected = ref<Set<string>>(new Set());
|
|
|
|
|
|
const menuId = ref<string | null>(null);
|
|
|
|
|
|
const filterSheet = ref(false);
|
|
|
|
|
|
const addOpen = ref(false);
|
2026-09-10 01:22:24 +03:30
|
|
|
|
const invoiceModal = ref<{ kind: 'invoice' | 'credit' | 'quote' } | null>(null);
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
|
|
|
|
|
function mapInvoiceStatus(inv: InvoiceDto): { key: PayFilter; label: string } {
|
|
|
|
|
|
if (inv.status === 'paid') return { key: 'paid', label: 'پرداخت شده' };
|
|
|
|
|
|
if (inv.status === 'draft') return { key: 'pending', label: 'در انتظار تایید' };
|
|
|
|
|
|
if (inv.status === 'canceled') return { key: 'expired', label: 'لغو شده' };
|
|
|
|
|
|
if (inv.status === 'partially_paid') return { key: 'unpaid', label: 'پرداخت جزئی' };
|
|
|
|
|
|
return { key: 'unpaid', label: 'پرداخت نشده' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mapQuoteStatus(qt: QuoteDto): { key: PayFilter | 'quote'; label: string } {
|
|
|
|
|
|
if (qt.status === 'expired') return { key: 'expired', label: 'منقضی شده' };
|
|
|
|
|
|
if (qt.status === 'sent') return { key: 'pending', label: 'در انتظار تایید' };
|
|
|
|
|
|
if (qt.status === 'accepted') return { key: 'paid', label: 'تأیید شده' };
|
|
|
|
|
|
if (qt.status === 'rejected') return { key: 'unpaid', label: 'رد شده' };
|
|
|
|
|
|
return { key: 'quote', label: 'پیش فاکتور' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function customerOf(row: {
|
|
|
|
|
|
company?: { id?: string; name: string } | null;
|
|
|
|
|
|
person?: {
|
|
|
|
|
|
id?: string;
|
|
|
|
|
|
firstName?: string;
|
|
|
|
|
|
lastName?: string;
|
|
|
|
|
|
fullName?: string;
|
|
|
|
|
|
} | null;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const company = row.company?.name?.trim() || '';
|
|
|
|
|
|
const person = row.person
|
|
|
|
|
|
? (
|
|
|
|
|
|
row.person.fullName ||
|
|
|
|
|
|
`${row.person.firstName ?? ''} ${row.person.lastName ?? ''}`
|
|
|
|
|
|
).trim()
|
|
|
|
|
|
: '';
|
|
|
|
|
|
return {
|
|
|
|
|
|
customerName: company || person || 'بدون مشتری',
|
|
|
|
|
|
personName: company && person ? person : '',
|
|
|
|
|
|
personId: row.person?.id ?? null,
|
|
|
|
|
|
companyId: row.company?.id ?? null,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rows = computed<BillingRow[]>(() => {
|
|
|
|
|
|
const invRows: BillingRow[] = invoices.value.map((inv) => {
|
|
|
|
|
|
const st = mapInvoiceStatus(inv);
|
|
|
|
|
|
const cust = customerOf(inv);
|
|
|
|
|
|
const kind = inv.kind === 'credit' ? 'credit' : 'invoice';
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: `inv:${inv.id}`,
|
|
|
|
|
|
entityId: inv.id,
|
|
|
|
|
|
personId: cust.personId,
|
|
|
|
|
|
companyId: cust.companyId,
|
|
|
|
|
|
doc: kind,
|
|
|
|
|
|
number: inv.number,
|
|
|
|
|
|
date: inv.issuedAt || inv.createdAt,
|
|
|
|
|
|
dueOrValid: inv.dueDate,
|
|
|
|
|
|
customerName: cust.customerName,
|
|
|
|
|
|
personName: cust.personName,
|
|
|
|
|
|
amount: inv.payableAmount ?? inv.totalAmount,
|
|
|
|
|
|
currency: inv.currency,
|
|
|
|
|
|
statusKey: st.key,
|
|
|
|
|
|
statusLabel: st.label,
|
|
|
|
|
|
href: { name: 'invoice-detail', params: { id: inv.id } },
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
const quoteRows: BillingRow[] = quotes.value.map((qt) => {
|
|
|
|
|
|
const st = mapQuoteStatus(qt);
|
|
|
|
|
|
const cust = customerOf(qt);
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: `qt:${qt.id}`,
|
|
|
|
|
|
entityId: qt.id,
|
|
|
|
|
|
personId: cust.personId,
|
|
|
|
|
|
companyId: cust.companyId,
|
|
|
|
|
|
doc: 'quote',
|
|
|
|
|
|
number: qt.number,
|
|
|
|
|
|
date: qt.issuedAt || qt.createdAt,
|
|
|
|
|
|
dueOrValid: qt.validUntil,
|
|
|
|
|
|
customerName: cust.customerName,
|
|
|
|
|
|
personName: cust.personName,
|
|
|
|
|
|
amount: qt.payableAmount ?? qt.totalAmount,
|
|
|
|
|
|
currency: qt.currency,
|
|
|
|
|
|
statusKey: st.key,
|
|
|
|
|
|
statusLabel: st.label,
|
|
|
|
|
|
href: { name: 'quote-detail', params: { id: qt.id } },
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
return [...invRows, ...quoteRows].sort(
|
|
|
|
|
|
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const counts = computed(() => ({
|
|
|
|
|
|
all: rows.value.length,
|
|
|
|
|
|
invoice: rows.value.filter((r) => r.doc === 'invoice').length,
|
|
|
|
|
|
quote: rows.value.filter((r) => r.doc === 'quote').length,
|
|
|
|
|
|
credit: rows.value.filter((r) => r.doc === 'credit').length,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const filtered = computed(() => {
|
|
|
|
|
|
let list = rows.value;
|
|
|
|
|
|
if (docTab.value !== 'all') list = list.filter((r) => r.doc === docTab.value);
|
|
|
|
|
|
if (payFilter.value !== 'all') {
|
|
|
|
|
|
list = list.filter((r) => r.statusKey === payFilter.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (personId.value) {
|
|
|
|
|
|
list = list.filter((r) => r.personId === personId.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (from.value) {
|
|
|
|
|
|
const t = new Date(from.value).getTime();
|
|
|
|
|
|
list = list.filter((r) => new Date(r.date).getTime() >= t);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (to.value) {
|
|
|
|
|
|
const t = new Date(to.value).getTime() + 86400000 - 1;
|
|
|
|
|
|
list = list.filter((r) => new Date(r.date).getTime() <= t);
|
|
|
|
|
|
}
|
|
|
|
|
|
const amin = Number(amountMin.value);
|
|
|
|
|
|
const amax = Number(amountMax.value);
|
|
|
|
|
|
if (Number.isFinite(amin) && amountMin.value !== '') {
|
|
|
|
|
|
list = list.filter((r) => Math.abs(r.amount) >= amin);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Number.isFinite(amax) && amountMax.value !== '') {
|
|
|
|
|
|
list = list.filter((r) => Math.abs(r.amount) <= amax);
|
|
|
|
|
|
}
|
2026-10-06 15:04:46 +03:30
|
|
|
|
if (q.value.trim()) {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
list = list.filter((r) =>
|
2026-10-06 15:04:46 +03:30
|
|
|
|
matchesSearch(q.value, [r.number, r.customerName, r.personName, r.statusLabel, String(r.amount)]),
|
2026-09-05 17:53:50 +03:30
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return list;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const pageCount = computed(() =>
|
|
|
|
|
|
Math.max(1, Math.ceil(filtered.value.length / pageSize.value)),
|
|
|
|
|
|
);
|
|
|
|
|
|
const pageItems = computed(() => {
|
|
|
|
|
|
const start = (page.value - 1) * pageSize.value;
|
|
|
|
|
|
return filtered.value.slice(start, start + pageSize.value);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function docLabel(doc: BillingRow['doc']) {
|
|
|
|
|
|
if (doc === 'quote') return 'پیش فاکتور';
|
|
|
|
|
|
if (doc === 'credit') return 'فاکتور برگشتی';
|
|
|
|
|
|
return 'فاکتور';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function statusIcon(key: BillingRow['statusKey']) {
|
|
|
|
|
|
if (key === 'paid') return 'check' as const;
|
|
|
|
|
|
if (key === 'pending') return 'clock' as const;
|
|
|
|
|
|
if (key === 'unpaid') return 'warning' as const;
|
|
|
|
|
|
if (key === 'expired') return 'minus' as const;
|
|
|
|
|
|
return 'document' as const;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
function statusColor(key: BillingRow['statusKey']) {
|
|
|
|
|
|
const icon = statusIcon(key);
|
|
|
|
|
|
if (icon === 'check') return 'success';
|
|
|
|
|
|
if (icon === 'clock') return 'processing';
|
|
|
|
|
|
if (icon === 'warning') return 'warning';
|
|
|
|
|
|
if (icon === 'minus') return 'error';
|
|
|
|
|
|
return 'default';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
function formatDate(iso: string | null | undefined) {
|
|
|
|
|
|
if (!iso) return '—';
|
|
|
|
|
|
try {
|
|
|
|
|
|
return formatJalaliNumeric(new Date(iso));
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return '—';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function money(n: number, currency: string) {
|
|
|
|
|
|
return formatMoney(n, currency);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeMenus() {
|
|
|
|
|
|
menuId.value = null;
|
|
|
|
|
|
addOpen.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setDocTab(tab: DocTab) {
|
|
|
|
|
|
docTab.value = tab;
|
|
|
|
|
|
page.value = 1;
|
|
|
|
|
|
void router.replace({
|
|
|
|
|
|
name: 'invoices',
|
|
|
|
|
|
query: {
|
|
|
|
|
|
...route.query,
|
|
|
|
|
|
doc: tab === 'all' ? undefined : tab,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearFilters() {
|
|
|
|
|
|
payFilter.value = 'all';
|
|
|
|
|
|
personId.value = '';
|
|
|
|
|
|
from.value = '';
|
|
|
|
|
|
to.value = '';
|
|
|
|
|
|
amountMin.value = '';
|
|
|
|
|
|
amountMax.value = '';
|
|
|
|
|
|
q.value = '';
|
|
|
|
|
|
page.value = 1;
|
|
|
|
|
|
filterSheet.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyFilters() {
|
|
|
|
|
|
page.value = 1;
|
|
|
|
|
|
filterSheet.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleSelectAll() {
|
|
|
|
|
|
const next = new Set(selected.value);
|
|
|
|
|
|
if (pageItems.value.every((r) => next.has(r.id))) {
|
|
|
|
|
|
for (const r of pageItems.value) next.delete(r.id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (const r of pageItems.value) next.add(r.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
selected.value = next;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleSelect(id: string) {
|
|
|
|
|
|
const next = new Set(selected.value);
|
|
|
|
|
|
if (next.has(id)) next.delete(id);
|
|
|
|
|
|
else next.add(id);
|
|
|
|
|
|
selected.value = next;
|
|
|
|
|
|
}
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
error.value = '';
|
2026-09-05 17:53:50 +03:30
|
|
|
|
closeMenus();
|
2026-08-29 21:06:30 +03:30
|
|
|
|
try {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
const [invRes, quoteRes, peopleRes] = await Promise.all([
|
2026-09-09 13:12:13 +03:30
|
|
|
|
api.get<PaginatedResponse<InvoiceDto>>('/invoices', { params: { limit: 100 } }),
|
|
|
|
|
|
api.get<PaginatedResponse<QuoteDto>>('/quotes', { params: { limit: 100 } }),
|
|
|
|
|
|
api.get<PaginatedResponse<PersonDto>>('/people', { params: { limit: 100 } }),
|
2026-09-05 17:53:50 +03:30
|
|
|
|
]);
|
|
|
|
|
|
invoices.value = invRes.data.items;
|
|
|
|
|
|
quotes.value = quoteRes.data.items;
|
|
|
|
|
|
people.value = peopleRes.data.items;
|
2026-08-29 21:06:30 +03:30
|
|
|
|
} catch (e) {
|
2026-09-05 17:53:50 +03:30
|
|
|
|
error.value = axios.isAxiosError(e)
|
|
|
|
|
|
? String(
|
|
|
|
|
|
(e.response?.data as { message?: string })?.message ??
|
|
|
|
|
|
'بارگذاری اسناد ناموفق بود.',
|
|
|
|
|
|
)
|
|
|
|
|
|
: 'بارگذاری اسناد ناموفق بود.';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 17:53:50 +03:30
|
|
|
|
watch([docTab, pageSize], () => {
|
|
|
|
|
|
page.value = 1;
|
|
|
|
|
|
});
|
2026-09-05 02:29:41 +03:30
|
|
|
|
|
2026-08-29 21:06:30 +03:30
|
|
|
|
watch(
|
2026-09-05 17:53:50 +03:30
|
|
|
|
() => route.query.doc,
|
|
|
|
|
|
(doc) => {
|
|
|
|
|
|
if (doc === 'quote' || doc === 'credit' || doc === 'invoice') docTab.value = doc;
|
|
|
|
|
|
else if (!doc) docTab.value = 'all';
|
2026-08-29 21:06:30 +03:30
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(load);
|
2026-10-18 19:39:59 +03:30
|
|
|
|
|
|
|
|
|
|
const docTabOptions = computed(() => [
|
|
|
|
|
|
{ value: 'all', label: `همه (${toFaDigits(counts.value.all)})` },
|
|
|
|
|
|
{ value: 'invoice', label: `فاکتور (${toFaDigits(counts.value.invoice)})` },
|
|
|
|
|
|
{ value: 'quote', label: `پیش فاکتور (${toFaDigits(counts.value.quote)})` },
|
|
|
|
|
|
{ value: 'credit', label: `فاکتور برگشتی (${toFaDigits(counts.value.credit)})` },
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const payFilterOptions = [
|
|
|
|
|
|
{ value: 'all', label: 'همه وضعیتها' },
|
|
|
|
|
|
{ value: 'paid', label: 'پرداخت شده' },
|
|
|
|
|
|
{ value: 'unpaid', label: 'پرداخت نشده' },
|
|
|
|
|
|
{ value: 'pending', label: 'در انتظار تایید' },
|
|
|
|
|
|
{ value: 'expired', label: 'منقضی شده' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const personOptions = computed(() =>
|
|
|
|
|
|
people.value.map((p) => ({
|
|
|
|
|
|
value: p.id,
|
|
|
|
|
|
label: p.fullName || `${p.firstName} ${p.lastName}`,
|
|
|
|
|
|
})),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const modalTitle = computed(() => {
|
|
|
|
|
|
if (invoiceModal.value?.kind === 'quote') return 'پیش فاکتور جدید';
|
|
|
|
|
|
if (invoiceModal.value?.kind === 'credit') return 'فاکتور برگشتی';
|
|
|
|
|
|
return 'فاکتور جدید';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const selectedKeys = computed({
|
|
|
|
|
|
get: () => [...selected.value],
|
|
|
|
|
|
set: (keys: string[]) => {
|
|
|
|
|
|
selected.value = new Set(keys);
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const columns = computed<TableColumnsType<BillingRow>>(() => {
|
|
|
|
|
|
const all: TableColumnsType<BillingRow> = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '#',
|
|
|
|
|
|
key: 'index',
|
|
|
|
|
|
width: 56,
|
|
|
|
|
|
customRender: ({ index }) =>
|
|
|
|
|
|
toFaDigits((page.value - 1) * pageSize.value + index + 1),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'شماره فاکتور',
|
|
|
|
|
|
dataIndex: 'number',
|
|
|
|
|
|
key: 'شماره فاکتور',
|
|
|
|
|
|
customRender: ({ text }) => ({
|
|
|
|
|
|
children: text,
|
|
|
|
|
|
props: { dir: 'ltr' },
|
|
|
|
|
|
}),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'تاریخ',
|
|
|
|
|
|
key: 'تاریخ',
|
|
|
|
|
|
customRender: ({ record }) => formatDate(record.date),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'مشتری',
|
|
|
|
|
|
key: 'مشتری',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'نوع',
|
|
|
|
|
|
key: 'نوع',
|
|
|
|
|
|
customRender: ({ record }) => docLabel(record.doc),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'وضعیت',
|
|
|
|
|
|
key: 'وضعیت',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'مبلغ کل',
|
|
|
|
|
|
key: 'مبلغ کل',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'ارز',
|
|
|
|
|
|
dataIndex: 'currency',
|
|
|
|
|
|
key: 'ارز',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'مهلت پرداخت',
|
|
|
|
|
|
key: 'مهلت پرداخت',
|
|
|
|
|
|
customRender: ({ record }) => formatDate(record.dueOrValid),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'عملیات',
|
|
|
|
|
|
key: 'عملیات',
|
|
|
|
|
|
fixed: 'left',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
return all.filter((col) => {
|
|
|
|
|
|
const key = String(col.key ?? '');
|
|
|
|
|
|
if (key === 'index') return true;
|
|
|
|
|
|
if (!INVOICE_COL_LABELS.includes(key as (typeof INVOICE_COL_LABELS)[number])) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return tableCols.isVisible(key);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function onAddMenu({ key }: { key: string | number }) {
|
|
|
|
|
|
const kind = String(key) as 'invoice' | 'quote' | 'credit';
|
|
|
|
|
|
invoiceModal.value = { kind };
|
|
|
|
|
|
addOpen.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onRowMenu({ key }: { key: string | number }, row: BillingRow) {
|
|
|
|
|
|
if (String(key) === 'open') {
|
|
|
|
|
|
void router.push(row.href);
|
|
|
|
|
|
}
|
|
|
|
|
|
menuId.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onDocTabChange(value: unknown) {
|
|
|
|
|
|
setDocTab(String(value ?? 'all') as DocTab);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onPersonFilterChange(value: unknown) {
|
|
|
|
|
|
personId.value = value == null ? '' : String(value);
|
|
|
|
|
|
page.value = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onAmountMinChange(value: number | string | null) {
|
|
|
|
|
|
amountMin.value = value == null ? '' : String(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onAmountMaxChange(value: number | string | null) {
|
|
|
|
|
|
amountMax.value = value == null ? '' : String(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onTableSelectChange(keys: (string | number)[]) {
|
|
|
|
|
|
const next = keys.map(String);
|
|
|
|
|
|
const prev = selected.value;
|
|
|
|
|
|
if (Math.abs(next.length - prev.size) === 1) {
|
|
|
|
|
|
const nextSet = new Set(next);
|
|
|
|
|
|
for (const id of nextSet) {
|
|
|
|
|
|
if (!prev.has(id)) {
|
|
|
|
|
|
toggleSelect(id);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const id of prev) {
|
|
|
|
|
|
if (!nextSet.has(id)) {
|
|
|
|
|
|
toggleSelect(id);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
selectedKeys.value = next;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
defineExpose({ toggleSelect, pageCount, statusIcon });
|
2026-08-29 21:06:30 +03:30
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<div @click="closeMenus">
|
|
|
|
|
|
<a-flex
|
|
|
|
|
|
justify="space-between"
|
|
|
|
|
|
align="center"
|
|
|
|
|
|
wrap="wrap"
|
|
|
|
|
|
:gap="12"
|
|
|
|
|
|
:style="{ marginBottom: '16px' }"
|
2026-09-12 01:33:07 +03:30
|
|
|
|
>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<div>
|
|
|
|
|
|
<a-typography-title :level="3" :style="{ margin: 0 }">فاکتور ها</a-typography-title>
|
|
|
|
|
|
<a-typography-text type="secondary">
|
|
|
|
|
|
مدیریت فاکتورهای فروش، پیش فاکتور و خدمات
|
|
|
|
|
|
</a-typography-text>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
</div>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-space :size="8" @click.stop>
|
|
|
|
|
|
<a-dropdown v-model:open="addOpen">
|
|
|
|
|
|
<a-button type="primary">
|
|
|
|
|
|
<template #icon><PlusOutlined /></template>
|
|
|
|
|
|
افزودن فاکتور
|
|
|
|
|
|
<DownOutlined />
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<template #overlay>
|
|
|
|
|
|
<a-menu @click="onAddMenu">
|
|
|
|
|
|
<a-menu-item key="invoice">فاکتور جدید</a-menu-item>
|
|
|
|
|
|
<a-menu-item key="quote">پیش فاکتور جدید</a-menu-item>
|
|
|
|
|
|
<a-menu-item key="credit">فاکتور برگشتی</a-menu-item>
|
|
|
|
|
|
</a-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-dropdown>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</a-flex>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-card :bordered="false">
|
|
|
|
|
|
<a-row :gutter="[16, 12]">
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
:value="docTab"
|
|
|
|
|
|
:options="docTabOptions"
|
|
|
|
|
|
style="width: 100%; max-width: 420px"
|
|
|
|
|
|
@update:value="onDocTabChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12" :lg="8">
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="q"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
placeholder="جستجو در شماره، مشتری، شرح ..."
|
|
|
|
|
|
@update:value="page = 1"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix><SearchOutlined /></template>
|
|
|
|
|
|
</a-input>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12" :lg="8">
|
|
|
|
|
|
<a-flex :gap="8" align="center">
|
|
|
|
|
|
<JalaliDateInput v-model="from" placeholder="از تاریخ" />
|
|
|
|
|
|
<a-typography-text type="secondary">–</a-typography-text>
|
|
|
|
|
|
<JalaliDateInput v-model="to" placeholder="تا تاریخ" />
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12" :lg="4">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="payFilter"
|
|
|
|
|
|
:options="payFilterOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@change="page = 1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12" :lg="4">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
:value="personId || undefined"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
show-search
|
|
|
|
|
|
option-filter-prop="label"
|
|
|
|
|
|
:options="personOptions"
|
|
|
|
|
|
placeholder="مشتری"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@update:value="onPersonFilterChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
|
|
|
|
|
|
<a-button @click="filterSheet = true">
|
|
|
|
|
|
<template #icon><FilterOutlined /></template>
|
|
|
|
|
|
فیلترهای بیشتر
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<TableColumnsMenu table-key="invoices" align="end" />
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
2026-08-29 21:06:30 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-alert
|
|
|
|
|
|
v-if="error"
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
:message="error"
|
|
|
|
|
|
:style="{ marginTop: '16px' }"
|
2026-09-12 01:33:07 +03:30
|
|
|
|
/>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-spin :spinning="loading">
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
v-if="!error"
|
|
|
|
|
|
:columns="columns"
|
|
|
|
|
|
:data-source="pageItems"
|
|
|
|
|
|
:pagination="false"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
:row-selection="{
|
|
|
|
|
|
selectedRowKeys: selectedKeys,
|
|
|
|
|
|
onChange: onTableSelectChange,
|
|
|
|
|
|
onSelectAll: toggleSelectAll,
|
|
|
|
|
|
}"
|
|
|
|
|
|
:style="{ marginTop: '16px' }"
|
|
|
|
|
|
:scroll="{ x: 'max-content' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.key === 'مشتری'">
|
|
|
|
|
|
<a-space :size="8">
|
|
|
|
|
|
<a-avatar>{{ record.customerName.slice(0, 1) }}</a-avatar>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div>{{ record.customerName }}</div>
|
|
|
|
|
|
<a-typography-text v-if="record.personName" type="secondary">
|
|
|
|
|
|
{{ record.personName }}
|
|
|
|
|
|
</a-typography-text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'وضعیت'">
|
|
|
|
|
|
<a-tag :color="statusColor(record.statusKey)" :bordered="false">
|
|
|
|
|
|
{{ record.statusLabel }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'مبلغ کل'">
|
|
|
|
|
|
<span dir="ltr">{{ money(record.amount, record.currency) }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'عملیات'">
|
|
|
|
|
|
<a-space :size="4" @click.stop>
|
|
|
|
|
|
<RouterLink :to="record.href">
|
|
|
|
|
|
<a-button type="text">
|
|
|
|
|
|
<template #icon><EyeOutlined /></template>
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</RouterLink>
|
|
|
|
|
|
<a-dropdown
|
|
|
|
|
|
:open="menuId === record.id"
|
|
|
|
|
|
@update:open="(v) => (menuId = v ? record.id : null)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-button type="text">
|
|
|
|
|
|
<template #icon><MoreOutlined /></template>
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<template #overlay>
|
|
|
|
|
|
<a-menu @click="(info) => onRowMenu(info, record as BillingRow)">
|
|
|
|
|
|
<a-menu-item key="open">باز کردن</a-menu-item>
|
|
|
|
|
|
</a-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-dropdown>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
|
|
|
|
|
|
<a-empty
|
|
|
|
|
|
v-if="!loading && !pageItems.length && !error"
|
|
|
|
|
|
description="سندی یافت نشد."
|
|
|
|
|
|
:style="{ marginTop: '24px' }"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<a-flex
|
|
|
|
|
|
v-if="filtered.length"
|
|
|
|
|
|
justify="flex-end"
|
|
|
|
|
|
:style="{ marginTop: '16px' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-pagination
|
|
|
|
|
|
v-model:current="page"
|
|
|
|
|
|
v-model:page-size="pageSize"
|
|
|
|
|
|
:total="filtered.length"
|
|
|
|
|
|
:show-size-changer="true"
|
|
|
|
|
|
:page-size-options="PAGE_SIZE_OPTIONS.map(String)"
|
|
|
|
|
|
:show-total="(total) => `${toFaDigits(total)} مورد · ${toFaDigits(pageCount)} صفحه`"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-flex>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
|
|
<a-modal
|
2026-09-10 01:22:24 +03:30
|
|
|
|
:open="Boolean(invoiceModal)"
|
2026-10-18 19:39:59 +03:30
|
|
|
|
:title="modalTitle"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
width="960px"
|
|
|
|
|
|
destroy-on-close
|
|
|
|
|
|
@cancel="invoiceModal = null"
|
2026-09-10 01:22:24 +03:30
|
|
|
|
>
|
|
|
|
|
|
<InvoiceFormView
|
|
|
|
|
|
v-if="invoiceModal"
|
|
|
|
|
|
embedded
|
|
|
|
|
|
:initial-kind="invoiceModal.kind"
|
|
|
|
|
|
@saved="invoiceModal = null; void load()"
|
|
|
|
|
|
@cancel="invoiceModal = null"
|
|
|
|
|
|
/>
|
2026-10-18 19:39:59 +03:30
|
|
|
|
</a-modal>
|
2026-09-05 17:53:50 +03:30
|
|
|
|
|
2026-10-18 19:39:59 +03:30
|
|
|
|
<a-drawer
|
|
|
|
|
|
v-model:open="filterSheet"
|
|
|
|
|
|
title="فیلترها"
|
|
|
|
|
|
placement="bottom"
|
|
|
|
|
|
:height="'80%'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-row :gutter="[16, 0]">
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="نوع سند">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
:value="docTab"
|
|
|
|
|
|
:options="docTabOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@update:value="onDocTabChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="وضعیت پرداخت">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="payFilter"
|
|
|
|
|
|
:options="payFilterOptions"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="24">
|
|
|
|
|
|
<a-form-item label="مشتری">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
:value="personId || undefined"
|
|
|
|
|
|
allow-clear
|
|
|
|
|
|
show-search
|
|
|
|
|
|
option-filter-prop="label"
|
|
|
|
|
|
:options="personOptions"
|
|
|
|
|
|
placeholder="جستجو و انتخاب مشتری ..."
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@update:value="onPersonFilterChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="از تاریخ">
|
|
|
|
|
|
<JalaliDateInput v-model="from" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="تا تاریخ">
|
|
|
|
|
|
<JalaliDateInput v-model="to" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="حداقل مبلغ">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
:value="amountMin === '' ? undefined : Number(amountMin)"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
placeholder="حداقل مبلغ"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@update:value="onAmountMinChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="12">
|
|
|
|
|
|
<a-form-item label="حداکثر مبلغ">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
:value="amountMax === '' ? undefined : Number(amountMax)"
|
|
|
|
|
|
:min="0"
|
|
|
|
|
|
placeholder="حداکثر مبلغ"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
@update:value="onAmountMaxChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
<a-space :size="8" :style="{ marginTop: '8px' }">
|
|
|
|
|
|
<a-button @click="clearFilters">پاک کردن</a-button>
|
|
|
|
|
|
<a-button type="primary" @click="applyFilters">اعمال فیلتر</a-button>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</a-drawer>
|
|
|
|
|
|
</div>
|
2026-08-29 21:06:30 +03:30
|
|
|
|
</template>
|