feat(marketplace): стол ПВЗ — склад/приёмка/раскладка под приёмочную модель
Десктоп к приёмочной модели склада (Путь A): - Приёмка партии: убраны кнопки-лаунчеры («Принять партию», «Принять самовывоз»). Приёмка запускается ТОЛЬКО сканом QR/вводом кода (идентификация обязательна). Ожидающие партии и самовывоз показаны карточками со составом («что везут» — товар + кол-во) без проваливания. - «Маркировка имущества» → «Раскладка и маркировка»: список принятых позиций склада; на строке — полка (свободный input), «Разложить» (split одной позиции по нескольким полкам), «Этикетка» (опциональный штрих-код). Снесены выбор партии и стратегии PER_ORDER/UNIT/PACKAGE. Печать этикеток сохранена. - Склад моего КУ: показывает всё принятое (статус RECEIVED «Принято»), колонка «Полка», штрих-код опционален (—), сортировка/возраст по received_at. SDK-обёртки inventory перегенерированы под новые мутации. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -443,12 +443,13 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
},
|
||||
},
|
||||
{
|
||||
// Эпик 5 / Story 5.8 + Эпик 6: operator-стол маркировки имущества.
|
||||
// Стол раскладки/маркировки: принятое имущество (склад) — назначить
|
||||
// полку, разложить позицию по нескольким полкам, наклеить штрих-код.
|
||||
path: 'labeling',
|
||||
name: 'marketplace-pvz-labeling',
|
||||
component: markRaw(OperatorInventoryLabelingPage),
|
||||
meta: {
|
||||
title: 'Маркировка имущества',
|
||||
title: 'Раскладка и маркировка',
|
||||
icon: 'fa-solid fa-tag',
|
||||
requires: 'Warehouse:read:own-KU',
|
||||
requiresAuth: true,
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
import { Mutations, Queries, Zeus } from '@coopenomics/sdk'
|
||||
import { Mutations, Queries } from '@coopenomics/sdk'
|
||||
import { client } from 'src/shared/api/client'
|
||||
|
||||
export interface MarketplaceOrderForLabeling {
|
||||
id: string;
|
||||
quantity: number;
|
||||
offer_id: string;
|
||||
orderer_account: string;
|
||||
delivery_braname: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// Zeus маппит скаляр ID в `unknown`; идентификатор переопределяем на строку
|
||||
// (используется как :key и для slice в UI).
|
||||
type _RawInventoryItem =
|
||||
Queries.Marketplace.ListInventory.IOutput['marketplaceListInventory'][number];
|
||||
export type MarketplaceInventoryItemView = Omit<_RawInventoryItem, 'id'> & { id: string };
|
||||
Queries.Marketplace.ListInventory.IOutput['marketplaceListInventory'][number]
|
||||
export type MarketplaceInventoryItemView = Omit<_RawInventoryItem, 'id'> & { id: string }
|
||||
|
||||
type _RawShipment =
|
||||
Queries.Marketplace.ListShipments.IOutput['marketplaceListShipments'][number];
|
||||
export type MarketplaceShipmentView = Omit<_RawShipment, 'id'> & { id: string };
|
||||
|
||||
export type MarketplaceLabelShipmentInventoryResultView =
|
||||
Mutations.Marketplace.LabelShipmentInventory.IOutput['marketplaceLabelShipmentInventory'];
|
||||
export type IListInventoryInput = Queries.Marketplace.ListInventory.IInput['data']
|
||||
export type IAssignShelfInput = Mutations.Marketplace.AssignInventoryShelf.IInput['data']
|
||||
export type ISplitInventoryInput = Mutations.Marketplace.SplitInventory.IInput['data']
|
||||
export type IGenerateLabelInput = Mutations.Marketplace.GenerateInventoryLabel.IInput['data']
|
||||
|
||||
export async function fetchInventoryByBraname(braname: string): Promise<MarketplaceInventoryItemView[]> {
|
||||
const { [Queries.Marketplace.ListInventory.name]: result } = await client.Query(
|
||||
@@ -32,40 +21,32 @@ export async function fetchInventoryByBraname(braname: string): Promise<Marketpl
|
||||
return result as MarketplaceInventoryItemView[]
|
||||
}
|
||||
|
||||
const SHIPMENT_STATUSES_FOR_LABELING: Zeus.MarketplaceShipmentStatus[] = [
|
||||
Zeus.MarketplaceShipmentStatus.SUPPLY_PREPARED,
|
||||
Zeus.MarketplaceShipmentStatus.RECEPTION_IN_PROGRESS,
|
||||
]
|
||||
|
||||
export async function fetchShipmentsForLabeling(braname?: string): Promise<MarketplaceShipmentView[]> {
|
||||
const data: Queries.Marketplace.ListShipments.IInput['data'] = {
|
||||
statuses: SHIPMENT_STATUSES_FOR_LABELING,
|
||||
}
|
||||
if (braname) data.braname = braname
|
||||
const { [Queries.Marketplace.ListShipments.name]: result } = await client.Query(
|
||||
Queries.Marketplace.ListShipments.query,
|
||||
export async function assignInventoryShelf(
|
||||
data: IAssignShelfInput,
|
||||
): Promise<MarketplaceInventoryItemView[]> {
|
||||
const { [Mutations.Marketplace.AssignInventoryShelf.name]: result } = await client.Mutation(
|
||||
Mutations.Marketplace.AssignInventoryShelf.mutation,
|
||||
{ variables: { data } },
|
||||
)
|
||||
// Zeus отдаёт ID как unknown; сужаем идентификатор до строки во view-типе.
|
||||
return result as MarketplaceShipmentView[]
|
||||
return result.inventory as MarketplaceInventoryItemView[]
|
||||
}
|
||||
|
||||
export async function labelInventory(
|
||||
data: Mutations.Marketplace.LabelInventory.IInput['data'],
|
||||
): Promise<Mutations.Marketplace.LabelInventory.IOutput['marketplaceLabelInventory']> {
|
||||
const { [Mutations.Marketplace.LabelInventory.name]: result } = await client.Mutation(
|
||||
Mutations.Marketplace.LabelInventory.mutation,
|
||||
export async function splitInventory(
|
||||
data: ISplitInventoryInput,
|
||||
): Promise<MarketplaceInventoryItemView[]> {
|
||||
const { [Mutations.Marketplace.SplitInventory.name]: result } = await client.Mutation(
|
||||
Mutations.Marketplace.SplitInventory.mutation,
|
||||
{ variables: { data } },
|
||||
)
|
||||
return result
|
||||
return result.inventory as MarketplaceInventoryItemView[]
|
||||
}
|
||||
|
||||
export async function labelShipmentInventory(
|
||||
data: Mutations.Marketplace.LabelShipmentInventory.IInput['data'],
|
||||
): Promise<MarketplaceLabelShipmentInventoryResultView> {
|
||||
const { [Mutations.Marketplace.LabelShipmentInventory.name]: result } = await client.Mutation(
|
||||
Mutations.Marketplace.LabelShipmentInventory.mutation,
|
||||
export async function generateInventoryLabel(
|
||||
data: IGenerateLabelInput,
|
||||
): Promise<MarketplaceInventoryItemView[]> {
|
||||
const { [Mutations.Marketplace.GenerateInventoryLabel.name]: result } = await client.Mutation(
|
||||
Mutations.Marketplace.GenerateInventoryLabel.mutation,
|
||||
{ variables: { data } },
|
||||
)
|
||||
return result
|
||||
return result.inventory as MarketplaceInventoryItemView[]
|
||||
}
|
||||
|
||||
+277
-230
@@ -1,177 +1,149 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Loading } from 'quasar'
|
||||
import { Zeus } from '@coopenomics/sdk'
|
||||
import { SuccessAlert, FailAlert } from 'src/shared/api'
|
||||
import { OperatorBranchBar, useOperatorBranchStore } from 'src/entities/OperatorBranch'
|
||||
import { BarcodeDisplay } from 'src/widgets/Marketplace/BarcodeDisplay'
|
||||
import { BaseButton, BaseCard, BaseInput, BaseSelect, EmptyState } from 'src/shared/ui/base'
|
||||
import type { BaseSelectOption } from 'src/shared/ui/base'
|
||||
import { BaseBadge, BaseButton, BaseDialog, BaseInput, EmptyState } from 'src/shared/ui/base'
|
||||
import { PageHint } from 'src/shared/ui/domain'
|
||||
import { Zeus } from '@coopenomics/sdk'
|
||||
import { RefreshButton } from 'src/widgets/Marketplace/RefreshButton'
|
||||
import {
|
||||
assignInventoryShelf,
|
||||
fetchInventoryByBraname,
|
||||
fetchShipmentsForLabeling,
|
||||
labelInventory,
|
||||
labelShipmentInventory,
|
||||
generateInventoryLabel,
|
||||
splitInventory,
|
||||
type MarketplaceInventoryItemView,
|
||||
type MarketplaceShipmentView,
|
||||
} from '../api'
|
||||
|
||||
type LabelingMode = 'PER_ORDER' | 'BATCH'
|
||||
type BarcodeStrategy = Zeus.MarketplaceBarcodeStrategy
|
||||
type BarcodeFormat = Zeus.MarketplaceBarcodeFormat
|
||||
/**
|
||||
* Стол ПВЗ, «Раскладка и маркировка». Имущество попадает на склад на приёмке
|
||||
* (статус RECEIVED) — здесь оператор организует его физически: назначает полку
|
||||
* (свободная строка), при необходимости раскладывает одну принятую позицию по
|
||||
* нескольким полкам (split), и опционально наклеивает штрих-код для быстрого
|
||||
* поиска. Штрих-код не обязателен — склад работает и без него (один холодильник).
|
||||
*/
|
||||
|
||||
// Активный КУ оператора — из общего контекста стола (без ввода кода вручную).
|
||||
const route = useRoute()
|
||||
const store = useOperatorBranchStore()
|
||||
const coopname = computed(() => String(route.params.coopname ?? ''))
|
||||
const braname = computed(() => store.activeBraname ?? '')
|
||||
|
||||
const items = ref<MarketplaceInventoryItemView[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const mode = ref<LabelingMode>('BATCH')
|
||||
const modeOptions: { label: string; value: LabelingMode }[] = [
|
||||
{ label: 'Маркировка партии целиком', value: 'BATCH' },
|
||||
{ label: 'Поштучно по заказу', value: 'PER_ORDER' },
|
||||
]
|
||||
// Сначала непромаркированные/без полки (нужно разложить), затем остальное.
|
||||
const RECEIVED = Zeus.MarketplaceInventoryStatus.RECEIVED
|
||||
const LABELED = Zeus.MarketplaceInventoryStatus.LABELED
|
||||
|
||||
const orderIdInput = ref<string>('')
|
||||
|
||||
const shipments = ref<MarketplaceShipmentView[]>([])
|
||||
const selectedShipmentId = ref<string | null>(null)
|
||||
const defaultStrategy = ref<BarcodeStrategy | null>(null)
|
||||
// EAN-13 — стандарт маркировки маркетплейса (UX-DR11/DR12), не QR/CODE128.
|
||||
const defaultFormat = ref<BarcodeFormat>(Zeus.MarketplaceBarcodeFormat.EAN13)
|
||||
const lastBatchSummary = ref<{ labeled: number; skipped: number } | null>(null)
|
||||
|
||||
// «Стратегия по умолчанию» = null (берётся из карточки товара). BaseSelect не
|
||||
// принимает null в value опций, поэтому используем строковый sentinel и
|
||||
// мапим его обратно в null через computed-прокси.
|
||||
const STRATEGY_DEFAULT = '__default__'
|
||||
|
||||
const strategyOptions: BaseSelectOption[] = [
|
||||
{ label: 'По умолчанию из карточки товара', value: STRATEGY_DEFAULT },
|
||||
{ label: 'Одна этикетка на весь заказ', value: Zeus.MarketplaceBarcodeStrategy.PER_ORDER },
|
||||
{ label: 'Отдельная этикетка на каждую единицу', value: Zeus.MarketplaceBarcodeStrategy.PER_UNIT },
|
||||
{ label: 'Этикетка на упаковку', value: Zeus.MarketplaceBarcodeStrategy.PER_PACKAGE },
|
||||
]
|
||||
|
||||
const formatOptions: BaseSelectOption[] = [
|
||||
{ label: 'CODE128', value: Zeus.MarketplaceBarcodeFormat.CODE128 },
|
||||
{ label: 'EAN13', value: Zeus.MarketplaceBarcodeFormat.EAN13 },
|
||||
]
|
||||
|
||||
const shipmentOptions = computed<BaseSelectOption[]>(() =>
|
||||
shipments.value.map((s) => ({
|
||||
label: `${s.id.slice(0, 8)} · ${s.braname} · ${s.delivery_variant} · ${s.status}`,
|
||||
value: s.id,
|
||||
})),
|
||||
const onWarehouse = computed(() =>
|
||||
[...items.value]
|
||||
.filter((i) => i.status === RECEIVED || i.status === LABELED)
|
||||
.sort((a, b) => {
|
||||
// Сперва то, чем ещё нужно заняться: без полки, затем без штрих-кода.
|
||||
const aw = (a.shelf ? 0 : 2) + (a.barcode_value ? 0 : 1)
|
||||
const bw = (b.shelf ? 0 : 2) + (b.barcode_value ? 0 : 1)
|
||||
return bw - aw
|
||||
}),
|
||||
)
|
||||
|
||||
const shipmentModel = computed<string>({
|
||||
get: () => selectedShipmentId.value ?? '',
|
||||
set: (v) => {
|
||||
selectedShipmentId.value = v || null
|
||||
},
|
||||
})
|
||||
const labeledItems = computed(() => items.value.filter((i) => !!i.barcode_value))
|
||||
|
||||
// Допустимые значения enum'ов — для type-guard без каста при приёме строки из
|
||||
// BaseSelect (sentinel STRATEGY_DEFAULT в список не входит → маппится в null).
|
||||
const STRATEGY_VALUES: readonly BarcodeStrategy[] = [
|
||||
Zeus.MarketplaceBarcodeStrategy.PER_ORDER,
|
||||
Zeus.MarketplaceBarcodeStrategy.PER_UNIT,
|
||||
Zeus.MarketplaceBarcodeStrategy.PER_PACKAGE,
|
||||
]
|
||||
function isStrategy(v: string): v is BarcodeStrategy {
|
||||
return STRATEGY_VALUES.some((s) => s === v)
|
||||
}
|
||||
// Черновик полки на строку — чтобы сохранять только по явному действию.
|
||||
const shelfDraft = reactive<Record<string, string>>({})
|
||||
|
||||
const FORMAT_VALUES: readonly BarcodeFormat[] = [
|
||||
Zeus.MarketplaceBarcodeFormat.CODE128,
|
||||
Zeus.MarketplaceBarcodeFormat.EAN13,
|
||||
]
|
||||
function isFormat(v: string): v is BarcodeFormat {
|
||||
return FORMAT_VALUES.some((f) => f === v)
|
||||
}
|
||||
|
||||
const strategyModel = computed<string>({
|
||||
get: () => defaultStrategy.value ?? STRATEGY_DEFAULT,
|
||||
set: (v) => {
|
||||
defaultStrategy.value = isStrategy(v) ? v : null
|
||||
},
|
||||
})
|
||||
|
||||
const formatModel = computed<string>({
|
||||
get: () => defaultFormat.value,
|
||||
set: (v) => {
|
||||
if (isFormat(v)) defaultFormat.value = v
|
||||
},
|
||||
})
|
||||
|
||||
async function loadInventory(): Promise<void> {
|
||||
if (!braname.value.trim()) return
|
||||
async function load(): Promise<void> {
|
||||
if (!braname.value.trim()) {
|
||||
items.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
items.value = await fetchInventoryByBraname(braname.value.trim())
|
||||
for (const i of items.value) shelfDraft[i.id] = i.shelf ?? ''
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить инвентарь')
|
||||
FailAlert(e, 'Не удалось загрузить склад участка')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadShipments(): Promise<void> {
|
||||
async function saveShelf(item: MarketplaceInventoryItemView): Promise<void> {
|
||||
const next = (shelfDraft[item.id] ?? '').trim()
|
||||
if (next === (item.shelf ?? '')) return
|
||||
try {
|
||||
shipments.value = await fetchShipmentsForLabeling(braname.value.trim() || undefined)
|
||||
await assignInventoryShelf({ inventory_id: item.id, shelf: next || null })
|
||||
SuccessAlert(next ? `Полка «${next}» сохранена` : 'Полка очищена')
|
||||
await load()
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить список партий')
|
||||
FailAlert(e, 'Не удалось сохранить полку')
|
||||
}
|
||||
}
|
||||
|
||||
async function generateLabelsPerOrder(): Promise<void> {
|
||||
if (!orderIdInput.value.trim()) {
|
||||
FailAlert(new Error('Укажите идентификатор заказа.'))
|
||||
return
|
||||
}
|
||||
Loading.show({ message: 'Генерирую этикетки…' })
|
||||
async function makeLabel(item: MarketplaceInventoryItemView): Promise<void> {
|
||||
Loading.show({ message: 'Генерирую этикетку…' })
|
||||
try {
|
||||
const result = await labelInventory({ order_id: orderIdInput.value.trim() })
|
||||
SuccessAlert(`Сгенерировано ${result.inventory.length} этикеток — можно печатать`)
|
||||
orderIdInput.value = ''
|
||||
await loadInventory()
|
||||
await generateInventoryLabel({ inventory_id: item.id })
|
||||
SuccessAlert('Этикетка сгенерирована — можно печатать')
|
||||
await load()
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось сгенерировать этикетки')
|
||||
FailAlert(e, 'Не удалось сгенерировать этикетку')
|
||||
} finally {
|
||||
Loading.hide()
|
||||
}
|
||||
}
|
||||
|
||||
async function generateLabelsForShipment(): Promise<void> {
|
||||
if (!selectedShipmentId.value) {
|
||||
FailAlert(new Error('Выберите партию для маркировки.'))
|
||||
return
|
||||
}
|
||||
Loading.show({ message: 'Маркирую партию…' })
|
||||
// ── Раскладка по полкам (split) ──
|
||||
const splitDialogOpen = ref(false)
|
||||
const splitTarget = ref<MarketplaceInventoryItemView | null>(null)
|
||||
const splitRows = ref<{ quantity: number | null; shelf: string }[]>([])
|
||||
|
||||
const splitTotal = computed(() =>
|
||||
splitRows.value.reduce((a, r) => a + (Number(r.quantity) || 0), 0),
|
||||
)
|
||||
const splitValid = computed(
|
||||
() =>
|
||||
!!splitTarget.value &&
|
||||
splitRows.value.length >= 1 &&
|
||||
splitRows.value.every((r) => Number(r.quantity) > 0) &&
|
||||
splitTotal.value === splitTarget.value.quantity_per_label,
|
||||
)
|
||||
|
||||
function openSplit(item: MarketplaceInventoryItemView): void {
|
||||
splitTarget.value = item
|
||||
// Предзаполняем двумя долями: текущая полка + пустая.
|
||||
splitRows.value = [
|
||||
{ quantity: item.quantity_per_label, shelf: item.shelf ?? '' },
|
||||
{ quantity: null, shelf: '' },
|
||||
]
|
||||
splitDialogOpen.value = true
|
||||
}
|
||||
|
||||
function addSplitRow(): void {
|
||||
splitRows.value.push({ quantity: null, shelf: '' })
|
||||
}
|
||||
function removeSplitRow(idx: number): void {
|
||||
splitRows.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
async function applySplit(): Promise<void> {
|
||||
if (!splitTarget.value || !splitValid.value) return
|
||||
const target = splitTarget.value
|
||||
splitDialogOpen.value = false
|
||||
Loading.show({ message: 'Раскладываю по полкам…' })
|
||||
try {
|
||||
const result = await labelShipmentInventory({
|
||||
shipment_id: selectedShipmentId.value,
|
||||
default_strategy: defaultStrategy.value ?? undefined,
|
||||
format: defaultFormat.value,
|
||||
await splitInventory({
|
||||
inventory_id: target.id,
|
||||
splits: splitRows.value.map((r) => ({
|
||||
quantity: Number(r.quantity),
|
||||
shelf: r.shelf.trim() || null,
|
||||
})),
|
||||
})
|
||||
lastBatchSummary.value = {
|
||||
labeled: result.labeled_order_ids.length,
|
||||
skipped: result.skipped_order_ids.length,
|
||||
}
|
||||
const skipNote = result.skipped_order_ids.length
|
||||
? ` Пропущено уже промаркированных: ${result.skipped_order_ids.length}.`
|
||||
: ''
|
||||
SuccessAlert(
|
||||
`Партия промаркирована. Заказов в работе: ${result.labeled_order_ids.length}.${skipNote} Готовы к печати ${result.inventory.length} этикеток.`,
|
||||
)
|
||||
await loadInventory()
|
||||
SuccessAlert(`Позиция разложена на ${splitRows.value.length} полок(и)`)
|
||||
await load()
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось промаркировать партию')
|
||||
FailAlert(e, 'Не удалось разложить позицию')
|
||||
} finally {
|
||||
Loading.hide()
|
||||
}
|
||||
@@ -181,153 +153,228 @@ function openPrintWindow(): void {
|
||||
window.print()
|
||||
}
|
||||
|
||||
watch(braname, () => {
|
||||
void loadInventory()
|
||||
void loadShipments()
|
||||
})
|
||||
watch(braname, () => void load())
|
||||
|
||||
onMounted(async () => {
|
||||
await store.ensureLoaded(coopname.value)
|
||||
await loadShipments()
|
||||
await loadInventory()
|
||||
void load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
q-page.labeling(role='region', aria-label='Маркировка имущества')
|
||||
q-page.place(role='region', aria-label='Раскладка и маркировка')
|
||||
OperatorBranchBar
|
||||
|
||||
EmptyState(
|
||||
v-if='!store.loading && !store.isOperator',
|
||||
title='Вы не оператор кооперативного участка',
|
||||
body='Маркировка имущества доступна председателю участка и его доверенным лицам.'
|
||||
body='Раскладка имущества доступна председателю участка и его доверенным лицам.'
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name='storefront', size='48px')
|
||||
|
||||
template(v-else)
|
||||
Teleport(to="#header-actions-host", defer)
|
||||
BaseButton(variant='secondary', size='sm', :disabled='!labeledItems.length', @click='openPrintWindow')
|
||||
template(#icon-left)
|
||||
q-icon(name='print', size='16px')
|
||||
| Печать этикеток
|
||||
RefreshButton(:loading='loading', @refresh='load')
|
||||
|
||||
PageHint.no-print(storage-key='mp:operator-labeling:banner-dismissed')
|
||||
| Сгенерируйте штрих-коды для прибывших заказов и распечатайте этикетки склада участка.
|
||||
|
||||
.labeling__toolbar.no-print
|
||||
q-btn-toggle(
|
||||
v-model='mode',
|
||||
no-caps,
|
||||
unelevated,
|
||||
toggle-color='primary',
|
||||
:options='modeOptions'
|
||||
)
|
||||
q-space
|
||||
BaseButton(variant='ghost', @click='loadShipments')
|
||||
template(#icon-left)
|
||||
q-icon(name='refresh', size='18px')
|
||||
| Обновить партии
|
||||
BaseButton(variant='secondary', :disabled='!items.length', @click='openPrintWindow')
|
||||
template(#icon-left)
|
||||
q-icon(name='print', size='18px')
|
||||
| Печать
|
||||
|
||||
BaseCard.labeling__panel.no-print(
|
||||
v-if='mode === "BATCH"',
|
||||
title='Маркировка партии целиком'
|
||||
)
|
||||
.t-muted.labeling__hint
|
||||
| Выберите партию поставки и стратегию по умолчанию. Сервер пройдёт по всем
|
||||
| заказам партии и сгенерирует этикетки; уже промаркированные пропускаются.
|
||||
.labeling__form-row
|
||||
BaseSelect.labeling__field(
|
||||
v-model='shipmentModel',
|
||||
label='Партия поставки',
|
||||
:options='shipmentOptions',
|
||||
:disabled='!shipmentOptions.length'
|
||||
)
|
||||
BaseSelect.labeling__field(
|
||||
v-model='strategyModel',
|
||||
label='Стратегия маркировки',
|
||||
:options='strategyOptions'
|
||||
)
|
||||
BaseSelect.labeling__field(
|
||||
v-model='formatModel',
|
||||
label='Формат штрих-кода',
|
||||
:options='formatOptions'
|
||||
)
|
||||
.labeling__actions
|
||||
BaseButton(variant='primary', :disabled='!selectedShipmentId', @click='generateLabelsForShipment')
|
||||
template(#icon-left)
|
||||
q-icon(name='qr_code_2', size='16px')
|
||||
| Промаркировать партию
|
||||
span.t-muted(v-if='lastBatchSummary')
|
||||
| Последний прогон: промаркировано {{ lastBatchSummary.labeled }}, пропущено {{ lastBatchSummary.skipped }}.
|
||||
span.t-muted(v-else)
|
||||
| Стратегия по умолчанию переопределяет настройку из карточки товара только если выбрана явно.
|
||||
|
||||
BaseCard.labeling__panel.no-print(v-else, title='Маркировка одного заказа')
|
||||
.labeling__form-row
|
||||
BaseInput.labeling__field(
|
||||
v-model='orderIdInput',
|
||||
label='Идентификатор заказа',
|
||||
placeholder='order_id',
|
||||
mono
|
||||
)
|
||||
BaseButton(variant='primary', @click='generateLabelsPerOrder')
|
||||
template(#icon-left)
|
||||
q-icon(name='qr_code_2', size='16px')
|
||||
| Сгенерировать этикетки
|
||||
.t-muted
|
||||
| Стратегия маркировки берётся из карточки товара поставщика.
|
||||
| Принятое имущество лежит на складе сразу. Назначьте каждой позиции полку
|
||||
| (свободная строка), при необходимости разложите одну позицию по нескольким
|
||||
| полкам и — по желанию — наклейте штрих-код для быстрого поиска. Штрих-код
|
||||
| не обязателен: если у вас один холодильник, можно обойтись без него.
|
||||
|
||||
EmptyState.no-print(
|
||||
v-if='!items.length',
|
||||
title='Этикеток пока нет',
|
||||
body='Промаркируйте партию или отдельный заказ — этикетки появятся ниже и будут готовы к печати.'
|
||||
v-if='!onWarehouse.length',
|
||||
title='На складе пусто',
|
||||
body='Здесь появятся принятые позиции — после приёмки партии на столе «Приёмка партии».'
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name='qr_code_2', size='48px')
|
||||
q-icon(name='inventory_2', size='48px')
|
||||
|
||||
.labeling__grid(v-else)
|
||||
.labeling__label(v-for='item in items', :key='item.id')
|
||||
.labeling__label-name.ellipsis {{ item.product_name_snapshot }}
|
||||
.labeling__label-meta.ellipsis
|
||||
.place__list.no-print(v-else)
|
||||
.place__item(v-for='item in onWarehouse', :key='item.id')
|
||||
.place__item-info
|
||||
.place__item-name {{ item.product_name_snapshot || 'Товар по предложению' }}
|
||||
.place__item-meta
|
||||
| Принято {{ item.quantity_per_label }} ед. · {{ item.orderer_account_snapshot }}
|
||||
BaseBadge(v-if='item.barcode_value', variant='pos') Промаркировано
|
||||
BaseBadge(v-else, variant='neutral') Без штрих-кода
|
||||
.place__item-controls
|
||||
BaseInput.place__shelf(
|
||||
v-model='shelfDraft[item.id]',
|
||||
label='Полка',
|
||||
placeholder='A-12',
|
||||
dense,
|
||||
@blur='saveShelf(item)',
|
||||
@keydown.enter='saveShelf(item)'
|
||||
)
|
||||
BaseButton(
|
||||
variant='ghost',
|
||||
size='sm',
|
||||
:disabled='!!item.barcode_value || item.quantity_per_label < 2',
|
||||
@click='openSplit(item)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='call_split', size='16px')
|
||||
| Разложить
|
||||
BaseButton(
|
||||
v-if='!item.barcode_value',
|
||||
variant='primary',
|
||||
size='sm',
|
||||
@click='makeLabel(item)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='qr_code_2', size='16px')
|
||||
| Этикетка
|
||||
|
||||
//- Печатная раскладка этикеток — только промаркированные позиции.
|
||||
.place__grid(v-if='labeledItems.length')
|
||||
.place__label(v-for='item in labeledItems', :key='item.id')
|
||||
.place__label-name.ellipsis {{ item.product_name_snapshot }}
|
||||
.place__label-meta.ellipsis
|
||||
| Пайщик: {{ item.orderer_account_snapshot }} · Кол-во: {{ item.quantity_per_label }}
|
||||
BarcodeDisplay(:code='item.barcode_value', size='md')
|
||||
template(v-if='item.shelf') · Полка: {{ item.shelf }}
|
||||
BarcodeDisplay(v-if='item.barcode_value', :code='item.barcode_value', size='md')
|
||||
|
||||
BaseDialog(v-model='splitDialogOpen', title='Разложить по полкам', size='md')
|
||||
.place__split(v-if='splitTarget')
|
||||
.place__split-head
|
||||
| {{ splitTarget.product_name_snapshot || 'Товар' }} — всего {{ splitTarget.quantity_per_label }} ед.
|
||||
.place__split-row(v-for='(row, idx) in splitRows', :key='idx')
|
||||
BaseInput.place__split-qty(
|
||||
v-model.number='row.quantity',
|
||||
type='number',
|
||||
label='Кол-во',
|
||||
dense
|
||||
)
|
||||
BaseInput.place__split-shelf(
|
||||
v-model='row.shelf',
|
||||
label='Полка',
|
||||
placeholder='A-12',
|
||||
dense
|
||||
)
|
||||
BaseButton(
|
||||
variant='ghost',
|
||||
size='sm',
|
||||
icon-only,
|
||||
:disabled='splitRows.length <= 1',
|
||||
aria-label='Удалить долю',
|
||||
@click='removeSplitRow(idx)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='close', size='16px')
|
||||
.place__split-foot
|
||||
BaseButton(variant='ghost', size='sm', @click='addSplitRow')
|
||||
template(#icon-left)
|
||||
q-icon(name='add', size='16px')
|
||||
| Ещё полка
|
||||
span.place__split-total(:class='{ "place__split-total--bad": splitTotal !== splitTarget.quantity_per_label }')
|
||||
| Сумма: {{ splitTotal }} / {{ splitTarget.quantity_per_label }}
|
||||
template(#footer)
|
||||
BaseButton(variant='ghost', size='sm', @click='splitDialogOpen = false') Отмена
|
||||
BaseButton(variant='primary', size='sm', :disabled='!splitValid', @click='applySplit') Разложить
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.labeling {
|
||||
.place {
|
||||
padding: var(--p-6, 24px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
|
||||
&__toolbar {
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-3, 12px);
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
background: var(--p-surface);
|
||||
padding: var(--p-3, 12px) var(--p-4, 16px);
|
||||
}
|
||||
|
||||
&__hint {
|
||||
margin-bottom: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
&__form-row {
|
||||
&__item-info {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-1, 4px);
|
||||
}
|
||||
|
||||
&__item-name {
|
||||
font-size: var(--p-fs-body, 14px);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
&__item-meta {
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__item-controls {
|
||||
display: flex;
|
||||
gap: var(--p-3, 12px);
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: var(--p-2, 8px);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__field {
|
||||
flex: 1 1 240px;
|
||||
min-width: 200px;
|
||||
&__shelf {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
&__split {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
&__split-head {
|
||||
font-size: var(--p-fs-body, 14px);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
&__split-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__split-qty {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
&__split-shelf {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__split-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-3, 12px);
|
||||
flex-wrap: wrap;
|
||||
margin-top: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
&__split-total {
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--p-ink-2);
|
||||
|
||||
&--bad {
|
||||
color: var(--p-danger, #b3261e);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__grid {
|
||||
@@ -356,7 +403,7 @@ q-page.labeling(role='region', aria-label='Маркировка имуществ
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.labeling {
|
||||
.place {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
@@ -365,14 +412,14 @@ q-page.labeling(role='region', aria-label='Маркировка имуществ
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
.labeling {
|
||||
.place {
|
||||
padding: 0;
|
||||
}
|
||||
.labeling__grid {
|
||||
.place__grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 4mm;
|
||||
}
|
||||
.labeling__label {
|
||||
.place__label {
|
||||
page-break-inside: avoid;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
+28
-10
@@ -24,7 +24,8 @@ const items = ref<MarketplaceInventoryItemView[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const statusOptions: { label: string; value: InventoryStatus }[] = [
|
||||
{ label: 'На складе', value: Zeus.MarketplaceInventoryStatus.LABELED },
|
||||
{ label: 'Принято', value: Zeus.MarketplaceInventoryStatus.RECEIVED },
|
||||
{ label: 'Промаркировано', value: Zeus.MarketplaceInventoryStatus.LABELED },
|
||||
{ label: 'Выдано пайщику', value: Zeus.MarketplaceInventoryStatus.ISSUED },
|
||||
{ label: 'Возврат на склад', value: Zeus.MarketplaceInventoryStatus.RETURNED },
|
||||
{ label: 'Списано', value: Zeus.MarketplaceInventoryStatus.WRITTEN_OFF },
|
||||
@@ -41,13 +42,14 @@ function toggleStatus(value: InventoryStatus): void {
|
||||
}
|
||||
|
||||
const columns: QTableProps['columns'] = [
|
||||
{ name: 'barcode_value', label: 'Штрих-код', field: 'barcode_value', align: 'left', sortable: true },
|
||||
{ name: 'shelf', label: 'Полка', field: 'shelf', align: 'left', sortable: true },
|
||||
{ name: 'product', label: 'Товар', field: 'product_name_snapshot', align: 'left', sortable: true },
|
||||
{ name: 'orderer', label: 'Заказчик', field: 'orderer_account_snapshot', align: 'left', sortable: true },
|
||||
{ name: 'quantity', label: 'Ед.', field: 'quantity_per_label', align: 'right', sortable: true },
|
||||
{ name: 'barcode_value', label: 'Штрих-код', field: 'barcode_value', align: 'left', sortable: true },
|
||||
{ name: 'status', label: 'Состояние', field: 'status', align: 'left', sortable: true },
|
||||
{ name: 'age', label: 'Возраст', field: 'labeled_at', align: 'right' },
|
||||
{ name: 'labeled_at', label: 'Промаркировано', field: 'labeled_at', align: 'left', sortable: true },
|
||||
{ name: 'age', label: 'Возраст', field: 'received_at', align: 'right' },
|
||||
{ name: 'received_at', label: 'Принято', field: 'received_at', align: 'left', sortable: true },
|
||||
]
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
@@ -67,6 +69,7 @@ const summary = computed(() => {
|
||||
return {
|
||||
byStatus,
|
||||
totalActive:
|
||||
(byStatus[Zeus.MarketplaceInventoryStatus.RECEIVED] ?? 0) +
|
||||
(byStatus[Zeus.MarketplaceInventoryStatus.LABELED] ?? 0) +
|
||||
(byStatus[Zeus.MarketplaceInventoryStatus.ISSUED] ?? 0) +
|
||||
(byStatus[Zeus.MarketplaceInventoryStatus.RETURNED] ?? 0),
|
||||
@@ -97,8 +100,10 @@ onMounted(async () => {
|
||||
|
||||
function humanStatus(status: string): string {
|
||||
switch (status) {
|
||||
case Zeus.MarketplaceInventoryStatus.RECEIVED:
|
||||
return 'Принято'
|
||||
case Zeus.MarketplaceInventoryStatus.LABELED:
|
||||
return 'На складе'
|
||||
return 'Промаркировано'
|
||||
case Zeus.MarketplaceInventoryStatus.ISSUED:
|
||||
return 'Выдано пайщику'
|
||||
case Zeus.MarketplaceInventoryStatus.RETURNED:
|
||||
@@ -112,6 +117,8 @@ function humanStatus(status: string): string {
|
||||
|
||||
function statusVariant(status: string): BaseBadgeVariant {
|
||||
switch (status) {
|
||||
case Zeus.MarketplaceInventoryStatus.RECEIVED:
|
||||
return 'neutral'
|
||||
case Zeus.MarketplaceInventoryStatus.LABELED:
|
||||
return 'info'
|
||||
case Zeus.MarketplaceInventoryStatus.ISSUED:
|
||||
@@ -158,7 +165,8 @@ q-page.warehouse(role='region', aria-label='Склад участка')
|
||||
|
||||
template(v-else)
|
||||
PageHint(storage-key='mp:operator-warehouse:banner-dismissed')
|
||||
| Промаркированное имущество вашего пункта выдачи — наклейки, заказчики и состояние.
|
||||
| Имущество, принятое на ваш пункт выдачи: что лежит на складе, на какой
|
||||
| полке, заказчик и состояние. Штрих-код есть не у всех позиций — он опционален.
|
||||
|
||||
.warehouse__filters
|
||||
.warehouse__chips
|
||||
@@ -202,21 +210,31 @@ q-page.warehouse(role='region', aria-label='Склад участка')
|
||||
:columns='columns',
|
||||
row-key='id',
|
||||
:loading='loading',
|
||||
:pagination='{ rowsPerPage: 25, sortBy: "labeled_at", descending: true }',
|
||||
:pagination='{ rowsPerPage: 25, sortBy: "received_at", descending: true }',
|
||||
:rows-per-page-options='[25, 50, 100, 0]',
|
||||
flat,
|
||||
bordered,
|
||||
binary-state-sort
|
||||
)
|
||||
template(#body-cell-shelf='props')
|
||||
q-td(:props='props')
|
||||
span(v-if='props.row.shelf') {{ props.row.shelf }}
|
||||
span.t-muted(v-else) —
|
||||
|
||||
template(#body-cell-barcode_value='props')
|
||||
q-td(:props='props')
|
||||
span.q-mono(v-if='props.row.barcode_value') {{ props.row.barcode_value }}
|
||||
span.t-muted(v-else) —
|
||||
|
||||
template(#body-cell-status='props')
|
||||
q-td(:props='props')
|
||||
BaseBadge(:variant='statusVariant(props.row.status)') {{ humanStatus(props.row.status) }}
|
||||
|
||||
template(#body-cell-age='props')
|
||||
q-td(:props='props') {{ formatAge(props.row.labeled_at) }}
|
||||
q-td(:props='props') {{ formatAge(props.row.received_at) }}
|
||||
|
||||
template(#body-cell-labeled_at='props')
|
||||
q-td(:props='props') {{ formatDateTime(props.row.labeled_at) }}
|
||||
template(#body-cell-received_at='props')
|
||||
q-td(:props='props') {{ formatDateTime(props.row.received_at) }}
|
||||
|
||||
template(#no-data)
|
||||
.warehouse__nodata
|
||||
|
||||
+63
-22
@@ -56,6 +56,9 @@ const expectedShipments = ref<MarketplaceShipmentView[]>([]);
|
||||
// Story 14.2: поставщики с принятыми заказами, ожидающими самовывоза на КУ
|
||||
// (партию заранее не формировали) — приёмка по факту присутствия.
|
||||
const expressCandidates = ref<MarketplaceExpressPickupCandidateView[]>([]);
|
||||
// Состав ожидаемого имущества по поставщику (для карточек «что везут» без
|
||||
// проваливания): грузим единицы поставщиков, чьи партии/самовывоз ждут приёмки.
|
||||
const ordersByOfferer = ref<Record<string, MarketplaceSupplierPickupOrderView[]>>({});
|
||||
const loading = ref(false);
|
||||
|
||||
// Партии, прибывшие на КУ и ожидающие создания акта приёмки: статус
|
||||
@@ -140,6 +143,7 @@ async function load(): Promise<void> {
|
||||
);
|
||||
expectedShipments.value = shipments;
|
||||
expressCandidates.value = express;
|
||||
await loadOffererContents();
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить акты приёмки');
|
||||
} finally {
|
||||
@@ -147,14 +151,39 @@ async function load(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Story 14.2: принять самовывоз по факту присутствия. Открываем ту же форму
|
||||
// коррекции, что и при сканировании QR (R5/B2): оператор правит фактическое
|
||||
// количество и цену по каждой единице — процесс приёмки единый, без исключений.
|
||||
// Сам express-акт создаётся из диалога по «Сформировать акты».
|
||||
async function acceptExpressPickup(
|
||||
candidate: MarketplaceExpressPickupCandidateView,
|
||||
): Promise<void> {
|
||||
await openPickupForSupplier(candidate.offerer_account);
|
||||
// Состав того, что ждёт приёмки, — для карточек «что везут». Грузим единицы
|
||||
// имущества по каждому поставщику, чья партия (SUPPLY_PREPARED) или самовывоз
|
||||
// ждёт на этом КУ. Сетевая нагрузка ограничена числом поставщиков на приёмке.
|
||||
async function loadOffererContents(): Promise<void> {
|
||||
const offerers = new Set<string>();
|
||||
for (const s of expectedShipments.value) {
|
||||
if (s.status === Zeus.MarketplaceShipmentStatus.SUPPLY_PREPARED) offerers.add(s.offerer_account);
|
||||
}
|
||||
for (const c of expressCandidates.value) offerers.add(c.offerer_account);
|
||||
const map: Record<string, MarketplaceSupplierPickupOrderView[]> = {};
|
||||
await Promise.all(
|
||||
[...offerers].map(async (account) => {
|
||||
try {
|
||||
map[account] = await listSupplierPickupOrders({
|
||||
braname: braname.value.trim(),
|
||||
offerer_account: account,
|
||||
});
|
||||
} catch {
|
||||
map[account] = [];
|
||||
}
|
||||
}),
|
||||
);
|
||||
ordersByOfferer.value = map;
|
||||
}
|
||||
|
||||
// Состав партии (задекларированные единицы по shipment_id).
|
||||
function shipmentContents(s: MarketplaceShipmentView): MarketplaceSupplierPickupOrderView[] {
|
||||
return (ordersByOfferer.value[s.offerer_account] ?? []).filter((o) => o.shipment_id === s.id);
|
||||
}
|
||||
|
||||
// Состав самовывоза по факту (добор по акцепту — без партии).
|
||||
function expressContents(c: MarketplaceExpressPickupCandidateView): MarketplaceSupplierPickupOrderView[] {
|
||||
return (ordersByOfferer.value[c.offerer_account] ?? []).filter((o) => o.status === 'ACCEPTED');
|
||||
}
|
||||
|
||||
// QR-код передачи (Эпик 14, агрегирующая приёмка): оператор сканирует
|
||||
@@ -426,12 +455,14 @@ q-page.reception(role='region', aria-label='Приёмка партии')
|
||||
| Сканировать QR
|
||||
|
||||
PageHint(storage-key='mp:operator-reception:banner-dismissed')
|
||||
| Партии, прибывшие на ваш пункт выдачи, ждут приёмки ниже. Выберите партию
|
||||
| (или отсканируйте QR поставщика), сверьте фактическое количество и цену
|
||||
| по каждой позиции, сформируйте акт и подпишите его председателем участка.
|
||||
| Партии, прибывшие на ваш пункт выдачи, и их состав — ниже. Приёмка
|
||||
| запускается ТОЛЬКО сканированием QR поставщика (или вводом кода) —
|
||||
| кнопка «Сканировать QR» в шапке. Это идентификация: без кода принять
|
||||
| нельзя, даже если знаете человека в лицо. Затем сверьте факт по
|
||||
| позициям, сформируйте акт и подпишите его председателем участка.
|
||||
|
||||
//- Ожидающие приёмки партии: выбор из списка вместо ручного ввода id.
|
||||
//- QR-сканер — для тех, кто принимает с телефона (Story 14.3).
|
||||
//- Ожидающие приёмки партии — информационные карточки «что везут».
|
||||
//- Запуск приёмки — только через скан QR/ввод кода (кнопка в шапке).
|
||||
.reception__pending
|
||||
.reception__pending-head
|
||||
.reception__pending-title Ожидают приёмки
|
||||
@@ -444,13 +475,12 @@ q-page.reception(role='region', aria-label='Приёмка партии')
|
||||
.reception__ship-meta
|
||||
| {{ SHIPMENT_VARIANT_LABEL[s.delivery_variant] ?? s.delivery_variant }} · {{ formatAsset2Digits(s.total_amount) }} ₽
|
||||
template(v-if='s.ttn_number') · ТТН {{ s.ttn_number }}
|
||||
BaseButton(variant='primary', size='sm', @click='openPickupForSupplier(s.offerer_account)')
|
||||
template(#icon-left)
|
||||
q-icon(name='how_to_reg', size='16px')
|
||||
| Принять партию
|
||||
ul.reception__contents(v-if='shipmentContents(s).length')
|
||||
li.reception__content-line(v-for='o in shipmentContents(s)', :key='o.id')
|
||||
| {{ o.product_name || 'Товар по предложению' }} — {{ o.quantity }} {{ marketplaceUnitShort(o.unit_of_measure) }}
|
||||
|
||||
//- Story 14.2: самовывоз по факту — поставщик приехал без заранее
|
||||
//- сформированной партии; оператор открывает приёмку по факту присутствия.
|
||||
//- сформированной партии. Тоже только через скан QR/ввод кода.
|
||||
.reception__pending(v-if='expressCandidates.length')
|
||||
.reception__pending-head
|
||||
.reception__pending-title Самовывоз по факту (без партии)
|
||||
@@ -460,10 +490,9 @@ q-page.reception(role='region', aria-label='Приёмка партии')
|
||||
.reception__ship-offerer {{ c.offerer_account }}
|
||||
.reception__ship-meta
|
||||
| Самовывоз · {{ c.orders_count }} заказ(ов) · {{ c.total_units }} ед. · {{ formatAsset2Digits(c.total_amount) }} ₽
|
||||
BaseButton(variant='secondary', size='sm', @click='acceptExpressPickup(c)')
|
||||
template(#icon-left)
|
||||
q-icon(name='how_to_reg', size='16px')
|
||||
| Принять самовывоз
|
||||
ul.reception__contents(v-if='expressContents(c).length')
|
||||
li.reception__content-line(v-for='o in expressContents(c)', :key='o.id')
|
||||
| {{ o.product_name || 'Товар по предложению' }} — {{ o.quantity }} {{ marketplaceUnitShort(o.unit_of_measure) }}
|
||||
|
||||
q-table.reception__table(
|
||||
:rows='items',
|
||||
@@ -652,6 +681,18 @@ q-page.reception(role='region', aria-label='Приёмка партии')
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
&__contents {
|
||||
margin: var(--p-2, 8px) 0 0;
|
||||
padding-left: var(--p-4, 16px);
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
&__content-line {
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__pickup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user