Files
ewa-mobile/src/apps/supply/views/BillDetailView.vue
T
2026-07-27 15:24:23 +05:00

284 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. 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 { 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>