fix
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import BillsView from "./views/BillsView.vue";
|
||||
import BillDetailView from "./views/BillDetailView.vue";
|
||||
|
||||
export const supplyRoutes = [
|
||||
{
|
||||
path: "/bills",
|
||||
name: "bills",
|
||||
component: BillsView,
|
||||
meta: { title: "Счета", requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/bills/:id",
|
||||
name: "bill-detail",
|
||||
component: BillDetailView,
|
||||
meta: { title: "Детали счета", back: true, requiresAuth: true },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,283 @@
|
||||
<script setup lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { showToast } from "vant";
|
||||
import { billApi } from "../../../generated/api";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
import DocumentApprovalTasks from "../../../shared/components/DocumentApprovalTasks.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const billId = computed(() => Number(route.params.id));
|
||||
const activeTab = ref("info");
|
||||
|
||||
const {
|
||||
item: bill,
|
||||
loadingItem,
|
||||
error,
|
||||
retrieve: loadBill,
|
||||
} = useModelApi(billApi, {
|
||||
retrieveErrorMessage: "Не удалось загрузить счет",
|
||||
autoLoad: false,
|
||||
autoLoadOnFilterChange: false,
|
||||
});
|
||||
|
||||
type BillRelated = Record<string, unknown>;
|
||||
|
||||
function parseJson(value: unknown): unknown {
|
||||
if (typeof value === "string") {
|
||||
if (!value.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseObject(value: unknown): BillRelated | null {
|
||||
const parsed = parseJson(value);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed as BillRelated;
|
||||
}
|
||||
|
||||
function objectLabel(value: unknown) {
|
||||
const record = parseObject(value);
|
||||
if (!record) {
|
||||
return "не указано";
|
||||
}
|
||||
|
||||
const candidates = ["name", "short_name", "number", "text", "full_name", "get_status_display"];
|
||||
for (const key of candidates) {
|
||||
const candidate = record[key];
|
||||
if (typeof candidate === "string" && candidate.trim()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return "не указано";
|
||||
}
|
||||
|
||||
function money(value: number | null | undefined) {
|
||||
if (typeof value !== "number") {
|
||||
return "не указано";
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
currency: "RUB",
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function formatValue(value: unknown) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "не указано";
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
async function openScan() {
|
||||
if (!bill.value?.scan) {
|
||||
showToast("У счета нет файла");
|
||||
return;
|
||||
}
|
||||
|
||||
await invoke("open_remote_file", { url: bill.value.scan });
|
||||
}
|
||||
|
||||
function relatedDocuments() {
|
||||
const raw = parseJson(bill.value?.transferdocument_set);
|
||||
if (!Array.isArray(raw)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (Number.isFinite(billId.value)) {
|
||||
loadBill(billId.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-notice-bar
|
||||
v-if="error"
|
||||
class="notice"
|
||||
color="#991b1b"
|
||||
background="#fee2e2"
|
||||
left-icon="warning-o"
|
||||
wrapable
|
||||
:scrollable="false"
|
||||
:text="error"
|
||||
/>
|
||||
|
||||
<section class="card detail-card">
|
||||
<van-loading v-if="loadingItem" class="state" type="spinner">Загрузка...</van-loading>
|
||||
|
||||
<van-empty v-else-if="!bill" description="Счет не найден" />
|
||||
|
||||
<template v-else>
|
||||
<div class="bill-summary">
|
||||
<div class="bill-summary__title">{{ bill.text || bill.number || `Счет #${bill.id}` }}</div>
|
||||
<div class="bill-summary__meta">
|
||||
№ {{ bill.number || 'не указан' }} · {{ bill.date_bill || bill.date }}
|
||||
</div>
|
||||
<div class="bill-summary__badges">
|
||||
<van-tag type="primary">{{ bill.status_name || bill.status }}</van-tag>
|
||||
<van-tag plain>{{ bill.contract_typ || 'не указан' }}</van-tag>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">Контрагент</div>
|
||||
<div class="detail-value">{{ objectLabel(bill.counterparty) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">Проект</div>
|
||||
<div class="detail-value">{{ objectLabel(bill.project) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">Договор</div>
|
||||
<div class="detail-value">{{ objectLabel(bill.contract) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell>
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">Сумма</div>
|
||||
<div class="detail-value detail-value--money">{{ money(bill.cost) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
|
||||
<van-tabs v-model:active="activeTab" shrink sticky class="bill-tabs">
|
||||
<van-tab name="info" title="Информация">
|
||||
<div class="tab-panel">
|
||||
<van-cell-group>
|
||||
<van-cell title="Номер" :value="formatValue(bill.number)" />
|
||||
<van-cell title="Описание" :value="formatValue(bill.text)" />
|
||||
<van-cell title="Дата счета" :value="formatValue(bill.date_bill)" />
|
||||
<van-cell title="Дата" :value="formatValue(bill.date)" />
|
||||
<van-cell title="Срок оплаты" :value="formatValue(bill.date_due)" />
|
||||
<van-cell title="Статус" :value="bill.status_name || bill.status" />
|
||||
<van-cell title="Тип" :value="formatValue(bill.contract_typ)" />
|
||||
<van-cell title="Комментарий" :value="formatValue(bill.comment)" />
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="finance" title="Финансы">
|
||||
<div class="tab-panel">
|
||||
<van-cell-group>
|
||||
<van-cell title="Сумма" :value="money(bill.cost)" />
|
||||
<van-cell title="Оплачено" :value="money(bill.paid)" />
|
||||
<van-cell title="К оплате" :value="money(bill.to_payd)" />
|
||||
<van-cell title="НДС" :value="money(bill.nds_cost)" />
|
||||
<van-cell title="Реестр" :value="bill.pp_maked ? 'Да' : 'Нет'" />
|
||||
<van-cell title="Архив" :value="bill.archive_s ? 'Да' : 'Нет'" />
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="relations" title="Связи">
|
||||
<div class="tab-panel">
|
||||
<van-cell-group>
|
||||
<van-cell title="Контрагент" :value="objectLabel(bill.counterparty)" />
|
||||
<van-cell title="Проект" :value="objectLabel(bill.project)" />
|
||||
<van-cell title="Договор" :value="objectLabel(bill.contract)" />
|
||||
<van-cell title="ФРЦ" :value="objectLabel(bill.frc)" />
|
||||
<van-cell title="Ответственный" :value="objectLabel(bill.responsible)" />
|
||||
<van-cell title="Автор" :value="objectLabel(bill.author)" />
|
||||
<van-cell title="Категория" :value="objectLabel(bill.category)" />
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group v-if="relatedDocuments().length" title="Документы">
|
||||
<van-cell v-for="(doc, index) in relatedDocuments()" :key="String((doc as Record<string, unknown>).id ?? index)">
|
||||
<template #title>
|
||||
<div class="detail-cell">
|
||||
<div class="detail-label">{{ String((doc as Record<string, unknown>).number ?? 'Документ') }}</div>
|
||||
<div class="detail-value">
|
||||
{{ String((doc as Record<string, unknown>).date ?? '') }}
|
||||
{{ (doc as Record<string, unknown>).cost ? `· ${formatValue((doc as Record<string, unknown>).cost)}` : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="approval" title="Согласование">
|
||||
<div class="tab-panel">
|
||||
<DocumentApprovalTasks
|
||||
v-if="bill"
|
||||
:document-id="bill.id"
|
||||
filter-name="bill"
|
||||
title="Согласование счета"
|
||||
/>
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab name="file" title="Файл">
|
||||
<div class="tab-panel">
|
||||
<van-empty v-if="!bill.scan" description="Файл не прикреплен" />
|
||||
<div v-else class="form-actions stacked-actions">
|
||||
<van-button block round type="primary" plain @click="openScan">
|
||||
Открыть файл
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bill-summary {
|
||||
padding: 16px 16px 10px;
|
||||
}
|
||||
|
||||
.bill-summary__title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.bill-summary__meta {
|
||||
margin-top: 4px;
|
||||
color: var(--van-text-color-2);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.bill-summary__badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,492 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { showToast } from "vant";
|
||||
import { billApi } from "../../../generated/api";
|
||||
import type { Bill, BillCreate, BillListParams, BillUpdate } from "../../../generated/models";
|
||||
import { useModelApi } from "../../../shared/composables/useModelApi";
|
||||
import CounterpartySelect from "../../contracts/components/CounterpartySelect.vue";
|
||||
import ContractCategorySelect from "../../contracts/components/ContractCategorySelect.vue";
|
||||
import FrcSelect from "../../contracts/components/FrcSelect.vue";
|
||||
import ProjectSelect from "../../contracts/components/ProjectSelect.vue";
|
||||
|
||||
type BillFilterParams = Omit<BillListParams, "counterparty" | "frc" | "project" | "category"> & {
|
||||
page?: number;
|
||||
counterparty?: number;
|
||||
frc?: number;
|
||||
project?: number;
|
||||
category?: number;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const router = useRouter();
|
||||
const {
|
||||
items: bills,
|
||||
filters,
|
||||
count,
|
||||
loading,
|
||||
error,
|
||||
load: loadBills,
|
||||
} = useModelApi<Bill, BillCreate, BillUpdate, BillListParams>(billApi, {
|
||||
defaultListParams: { ordering: "-id", page: 1 } as BillListParams,
|
||||
loadErrorMessage: "Не удалось загрузить счета",
|
||||
cleanListParams(params) {
|
||||
params.page = params.page || 1;
|
||||
params.text__contains = typeof params.text__contains === "string" ? params.text__contains.trim() || undefined : undefined;
|
||||
params.number__contains = typeof params.number__contains === "string" ? params.number__contains.trim() || undefined : undefined;
|
||||
params.status = params.status || undefined;
|
||||
params.status_name__contains = typeof params.status_name__contains === "string" ? params.status_name__contains.trim() || undefined : undefined;
|
||||
params.contract_typ = params.contract_typ || undefined;
|
||||
params.date = params.date || undefined;
|
||||
params.date_due = params.date_due || undefined;
|
||||
params.date_bill = params.date_bill || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const billFilters = filters as unknown as BillFilterParams;
|
||||
|
||||
const billTextContains = computed<string>({
|
||||
get() {
|
||||
return typeof filters.text__contains === "string" ? filters.text__contains : "";
|
||||
},
|
||||
set(value) {
|
||||
filters.text__contains = value || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const billNumberContains = computed<string>({
|
||||
get() {
|
||||
return typeof filters.number__contains === "string" ? filters.number__contains : "";
|
||||
},
|
||||
set(value) {
|
||||
filters.number__contains = value || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const billStatus = computed<string>({
|
||||
get() {
|
||||
return typeof filters.status === "string" ? filters.status : "";
|
||||
},
|
||||
set(value) {
|
||||
filters.status = value || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const billStatusNameContains = computed<string>({
|
||||
get() {
|
||||
return typeof filters.status_name__contains === "string" ? filters.status_name__contains : "";
|
||||
},
|
||||
set(value) {
|
||||
filters.status_name__contains = value || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const billContractTyp = computed<string>({
|
||||
get() {
|
||||
return typeof filters.contract_typ === "string" ? filters.contract_typ : "";
|
||||
},
|
||||
set(value) {
|
||||
filters.contract_typ = value || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const billDate = computed<string>({
|
||||
get() {
|
||||
return typeof filters.date === "string" ? filters.date : "";
|
||||
},
|
||||
set(value) {
|
||||
filters.date = value || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedDateBill = computed<string>({
|
||||
get() {
|
||||
return typeof filters.date_bill === "string" ? filters.date_bill : "";
|
||||
},
|
||||
set(value) {
|
||||
filters.date_bill = value || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedDateDue = computed<string>({
|
||||
get() {
|
||||
return typeof filters.date_due === "string" ? filters.date_due : "";
|
||||
},
|
||||
set(value) {
|
||||
filters.date_due = value || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const showFilters = ref(false);
|
||||
const syncing = ref(false);
|
||||
|
||||
const hasActiveFilters = computed(() =>
|
||||
Boolean(
|
||||
filters.text__contains ||
|
||||
filters.number__contains ||
|
||||
filters.status ||
|
||||
filters.status_name__contains ||
|
||||
filters.contract_typ ||
|
||||
filters.date ||
|
||||
filters.date_due ||
|
||||
filters.date_bill ||
|
||||
billFilters.counterparty ||
|
||||
billFilters.project ||
|
||||
billFilters.frc ||
|
||||
billFilters.category,
|
||||
),
|
||||
);
|
||||
|
||||
const activeFilterCount = computed(
|
||||
() =>
|
||||
[
|
||||
filters.text__contains,
|
||||
filters.number__contains,
|
||||
filters.status,
|
||||
filters.status_name__contains,
|
||||
filters.contract_typ,
|
||||
filters.date,
|
||||
filters.date_due,
|
||||
filters.date_bill,
|
||||
billFilters.counterparty,
|
||||
billFilters.project,
|
||||
billFilters.frc,
|
||||
billFilters.category,
|
||||
].filter(Boolean).length,
|
||||
);
|
||||
|
||||
const currentPage = computed({
|
||||
get() {
|
||||
return billFilters.page ?? 1;
|
||||
},
|
||||
set(page: number) {
|
||||
billFilters.page = page;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCounterpartyId = computed({
|
||||
get() {
|
||||
return billFilters.counterparty ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
billFilters.counterparty = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedProjectId = computed({
|
||||
get() {
|
||||
return billFilters.project ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
billFilters.project = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedFrcId = computed({
|
||||
get() {
|
||||
return billFilters.frc ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
billFilters.frc = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCategoryId = computed({
|
||||
get() {
|
||||
return billFilters.category ?? 0;
|
||||
},
|
||||
set(id: number) {
|
||||
billFilters.category = id || undefined;
|
||||
},
|
||||
});
|
||||
|
||||
function parseObject(value: unknown): Record<string, unknown> | null {
|
||||
if (typeof value === "string") {
|
||||
if (!value.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return parseObject(JSON.parse(value) as unknown);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function objectLabel(value: unknown) {
|
||||
const record = parseObject(value);
|
||||
if (!record) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const candidates = ["name", "short_name", "number", "text", "full_name", "get_status_display"];
|
||||
for (const key of candidates) {
|
||||
const candidate = record[key];
|
||||
if (typeof candidate === "string" && candidate.trim()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.text__contains = undefined;
|
||||
filters.number__contains = undefined;
|
||||
filters.status = undefined;
|
||||
filters.status_name__contains = undefined;
|
||||
filters.contract_typ = undefined;
|
||||
filters.date = undefined;
|
||||
filters.date_due = undefined;
|
||||
filters.date_bill = undefined;
|
||||
billFilters.counterparty = undefined;
|
||||
billFilters.project = undefined;
|
||||
billFilters.frc = undefined;
|
||||
billFilters.category = undefined;
|
||||
billFilters.page = 1;
|
||||
}
|
||||
|
||||
async function applyFilters() {
|
||||
billFilters.page = 1;
|
||||
showFilters.value = false;
|
||||
await loadBills();
|
||||
}
|
||||
|
||||
async function syncBills() {
|
||||
syncing.value = true;
|
||||
|
||||
try {
|
||||
await loadBills();
|
||||
showToast("Данные обновлены с сервера");
|
||||
} catch (err) {
|
||||
showToast(errorMessage(err, "Не удалось синхронизировать счета"));
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown, fallback: string) {
|
||||
if (typeof err === "object" && err && "detail" in err) {
|
||||
return String((err as { detail: unknown }).detail);
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
filters.text__contains,
|
||||
filters.number__contains,
|
||||
filters.status,
|
||||
filters.status_name__contains,
|
||||
filters.contract_typ,
|
||||
filters.date,
|
||||
filters.date_due,
|
||||
filters.date_bill,
|
||||
billFilters.counterparty,
|
||||
billFilters.project,
|
||||
billFilters.frc,
|
||||
billFilters.category,
|
||||
],
|
||||
() => {
|
||||
billFilters.page = 1;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-notice-bar
|
||||
v-if="error"
|
||||
class="notice"
|
||||
color="#991b1b"
|
||||
background="#fee2e2"
|
||||
left-icon="warning-o"
|
||||
wrapable
|
||||
:scrollable="false"
|
||||
:text="error"
|
||||
/>
|
||||
|
||||
<section class="bills-page">
|
||||
<van-search v-model="billTextContains" placeholder="Поиск по описанию" clearable />
|
||||
|
||||
<div class="bill-actions">
|
||||
<van-badge :content="activeFilterCount || undefined">
|
||||
<van-button size="small" plain type="primary" icon="filter-o" @click="showFilters = true">
|
||||
Фильтры
|
||||
</van-button>
|
||||
</van-badge>
|
||||
<van-button size="small" plain type="primary" :loading="loading" @click="loadBills()">
|
||||
Обновить
|
||||
</van-button>
|
||||
<van-button size="small" type="primary" :loading="syncing" @click="syncBills">
|
||||
Обновить с сервера
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<van-popup v-model:show="showFilters" round position="bottom" class="filters-popup">
|
||||
<div class="filters-sheet">
|
||||
<div class="filters-header">
|
||||
<h2>Фильтры</h2>
|
||||
<van-button size="small" plain type="primary" @click="resetFilters">
|
||||
Сбросить
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<van-cell-group class="bill-filters">
|
||||
<van-field
|
||||
v-model="billNumberContains"
|
||||
label="Номер"
|
||||
placeholder="Номер счета"
|
||||
clearable
|
||||
/>
|
||||
<van-field
|
||||
v-model="billStatus"
|
||||
label="Статус"
|
||||
placeholder="Код статуса"
|
||||
clearable
|
||||
/>
|
||||
<van-field
|
||||
v-model="billStatusNameContains"
|
||||
label="Статус текстом"
|
||||
placeholder="Название статуса"
|
||||
clearable
|
||||
/>
|
||||
<van-field
|
||||
v-model="billContractTyp"
|
||||
label="Тип"
|
||||
placeholder="contract_typ"
|
||||
clearable
|
||||
/>
|
||||
<van-field label="Контрагент">
|
||||
<template #input>
|
||||
<CounterpartySelect v-model="selectedCounterpartyId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="Объект">
|
||||
<template #input>
|
||||
<ProjectSelect v-model="selectedProjectId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="ЦФО">
|
||||
<template #input>
|
||||
<FrcSelect v-model="selectedFrcId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="Категория">
|
||||
<template #input>
|
||||
<ContractCategorySelect v-model="selectedCategoryId" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model="selectedDateBill" label="Дата счета" type="date" clearable />
|
||||
<van-field v-model="selectedDateDue" label="Срок оплаты" type="date" clearable />
|
||||
<van-field v-model="billDate" label="Создан" type="date" clearable />
|
||||
</van-cell-group>
|
||||
|
||||
<div class="filters-actions">
|
||||
<van-button block type="primary" @click="applyFilters">
|
||||
Применить
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<van-loading v-if="loading && bills.length === 0" class="state" type="spinner">
|
||||
Загрузка...
|
||||
</van-loading>
|
||||
|
||||
<van-empty
|
||||
v-else-if="bills.length === 0"
|
||||
:description="hasActiveFilters ? 'Счета не найдены' : 'Счетов пока нет'"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<van-cell-group>
|
||||
<van-cell
|
||||
v-for="bill in bills"
|
||||
:key="bill.id"
|
||||
:title="bill.text || bill.number || `Счет #${bill.id}`"
|
||||
:label="`№ ${bill.number || 'не указан'} · ${objectLabel(bill.counterparty) || 'не указан'}`"
|
||||
center
|
||||
is-link
|
||||
@click="router.push(`/bills/${bill.id}`)"
|
||||
>
|
||||
<template #right-icon>
|
||||
<van-tag :type="bill.status === '5' ? 'success' : bill.status === '3' ? 'warning' : 'primary'">
|
||||
{{ bill.status_name || bill.status }}
|
||||
</van-tag>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<van-pagination
|
||||
v-model="currentPage"
|
||||
class="bill-pagination"
|
||||
:total-items="count"
|
||||
:items-per-page="PAGE_SIZE"
|
||||
prev-text="Назад"
|
||||
next-text="Вперед"
|
||||
mode="simple"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bills-page {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.bill-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 12px 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.filters-popup {
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
.filters-sheet {
|
||||
padding: 14px 0 18px;
|
||||
}
|
||||
|
||||
.filters-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 16px 8px;
|
||||
}
|
||||
|
||||
.filters-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.bill-filters {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.filters-actions {
|
||||
padding: 4px 12px 0;
|
||||
}
|
||||
|
||||
.bill-pagination {
|
||||
margin: 10px 12px 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user