[598][@ant] refactor: списание — общий хелпер proposalDisplay (positionsLabel/proposalTitle) — убрать дубль из страницы и диалога

Склонение «позиций» и имя проекта по дате повторялись в AdminWriteoffsPage и
WriteoffProposalDetailsDialog — вынес в ../lib/proposalDisplay по правилу DRY.
This commit is contained in:
ant
2026-06-14 10:58:24 +00:00
parent e85d04a51e
commit 461f3b8db7
3 changed files with 25 additions and 28 deletions
@@ -0,0 +1,22 @@
import type { MarketplaceWriteoffProposalView } from '../api';
// Русское склонение «позиция / позиции / позиций».
export function positionsLabel(n: number): string {
const mod10 = n % 10;
const mod100 = n % 100;
let word = 'позиций';
if (mod10 === 1 && mod100 !== 11) word = 'позиция';
else if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) word = 'позиции';
return `${n} ${word}`;
}
function formatDate(value: string | null | undefined): string {
if (!value) return '—';
const d = new Date(value);
return Number.isNaN(d.getTime()) ? '—' : d.toLocaleDateString('ru-RU');
}
// Человеческое имя проекта списания — по дате подачи/цикла, не «N позиций».
export function proposalTitle(p: MarketplaceWriteoffProposalView): string {
return `Списание от ${formatDate(p.submitted_at ?? p.cycle_started_at ?? p.updated_at)}`;
}
@@ -18,6 +18,7 @@ import {
type MarketplaceWriteoffCandidateView,
type MarketplaceWriteoffProposalView,
} from '../api';
import { positionsLabel, proposalTitle } from '../lib/proposalDisplay';
import SubmitToCouncilDialog from './SubmitToCouncilDialog.vue';
import WriteoffProposalDetailsDialog from './WriteoffProposalDetailsDialog.vue';
@@ -206,21 +207,6 @@ function proposalDate(p: MarketplaceWriteoffProposalView): string | null | undef
return p.executed_at ?? p.rejected_at ?? p.submitted_at ?? p.updated_at;
}
// Русское склонение «позиция/позиции/позиций».
function positionsLabel(n: number): string {
const mod10 = n % 10;
const mod100 = n % 100;
let word = 'позиций';
if (mod10 === 1 && mod100 !== 11) word = 'позиция';
else if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) word = 'позиции';
return `${n} ${word}`;
}
// Человеческое имя проекта — по дате подачи/цикла, не «N позиций».
function proposalTitle(p: MarketplaceWriteoffProposalView): string {
const date = formatDate(p.submitted_at ?? p.cycle_started_at ?? p.updated_at);
return `Списание от ${date}`;
}
// Три состояния позиции на складе:
// · нет даты годности (гарантия 0) → «Без гарантии» — ручное списание сразу;
@@ -3,6 +3,7 @@ import { computed } from 'vue';
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
import { BaseDialog, BaseBadge } from 'src/shared/ui/base';
import type { MarketplaceWriteoffProposalView } from '../api';
import { positionsLabel, proposalTitle } from '../lib/proposalDisplay';
const props = defineProps<{
modelValue: boolean;
@@ -24,15 +25,6 @@ function fmtDateTime(value: string | null | undefined): string {
return Number.isNaN(d.getTime()) ? '—' : d.toLocaleString('ru-RU');
}
function positionsLabel(n: number): string {
const mod10 = n % 10;
const mod100 = n % 100;
let word = 'позиций';
if (mod10 === 1 && mod100 !== 11) word = 'позиция';
else if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) word = 'позиции';
return `${n} ${word}`;
}
function humanStatus(status: MarketplaceWriteoffProposalView['status']): string {
switch (status) {
case 'DRAFT':
@@ -84,10 +76,7 @@ function humanLog(action: string): string {
return LOG_LABELS[action] ?? action;
}
const title = computed(() => {
const p = props.proposal;
return `Списание от ${fmtDate(p.submitted_at ?? p.cycle_started_at ?? p.updated_at)}`;
});
const title = computed(() => proposalTitle(props.proposal));
const shortHash = computed(() => {
const h = props.proposal.proposal_hash;