refactor(marketplace): Эпик 15 desktop — КУ-чекбоксы + min-объём, batch-accept
- Форма оффера: вместо селектора типа поставки + target_volume — список КУ кооператива (BaseCheckbox + min-объём на каждый, через marketplaceListKUDetails); убраны cycleTypeOptions/isCollective/onSelectCycle/триггер-кнопка. - Стол поставщика: accept/decline через единый batch-API (acceptOrdersBatch/ declineOrdersBatch) по order_id[]; убрана cycle_id-маршрутизация. - Сводный заказ заказчика, модерация: убраны cycle-лейблы → сводка по КУ. - Удалены cycle-type-view алиасы, marketplaceCycleLabel; ESLint зелёный. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+9
-4
@@ -4,7 +4,7 @@ import { Dialog } from 'quasar';
|
||||
import { SuccessAlert, FailAlert } from 'src/shared/api';
|
||||
import { Queries } from '@coopenomics/sdk';
|
||||
import { client } from 'src/shared/api/client';
|
||||
import { marketplaceUnitShort, marketplaceCycleLabel } from 'src/shared/lib/consts';
|
||||
import { marketplaceUnitShort } from 'src/shared/lib/consts';
|
||||
import { marketplaceOfferImageUrls } from 'src/shared/lib/utils';
|
||||
import { BaseButton, EmptyState } from 'src/shared/ui/base';
|
||||
import { PageHint } from 'src/shared/ui/domain';
|
||||
@@ -50,6 +50,11 @@ function categoryName(offer: MarketplacePendingOfferView): string | null {
|
||||
return catId != null ? categoryNames.value[catId] ?? null : null;
|
||||
}
|
||||
|
||||
function deliveryPointsSummary(offer: MarketplacePendingOfferView): string {
|
||||
const points = offer.delivery_points ?? [];
|
||||
return points.map((p) => `${p.braname} (от ${p.min_supply_volume})`).join(', ');
|
||||
}
|
||||
|
||||
function toCatalogOffer(offer: MarketplacePendingOfferView): CatalogOffer {
|
||||
return {
|
||||
id: offer.id,
|
||||
@@ -188,9 +193,9 @@ q-page.moderation(role="region", aria-label="Модерация предложе
|
||||
.moderation__meta-row(v-if="categoryName(o)")
|
||||
span.moderation__meta-key Категория
|
||||
span.moderation__meta-val {{ categoryName(o) }}
|
||||
.moderation__meta-row(v-if="o.cycle_type")
|
||||
span.moderation__meta-key Тип отсечки
|
||||
span.moderation__meta-val {{ marketplaceCycleLabel(o.cycle_type) }}
|
||||
.moderation__meta-row(v-if="o.delivery_points && o.delivery_points.length")
|
||||
span.moderation__meta-key Участки поставки
|
||||
span.moderation__meta-val {{ deliveryPointsSummary(o) }}
|
||||
.moderation__meta-row(v-if="o.warranty_days != null && o.warranty_days > 0")
|
||||
span.moderation__meta-key Гарантия
|
||||
span.moderation__meta-val {{ o.warranty_days }} дн.
|
||||
|
||||
@@ -64,19 +64,3 @@ export async function withdrawOffer(id: string): Promise<void> {
|
||||
variables: { input: { id } },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Эпик 4 / Story 4.2: поставщик вручную запускает коллективную закупку по
|
||||
* своему предложению (cycle_type=collective).
|
||||
*
|
||||
* Backend Resolver: marketplace-cycle.resolver.ts → marketplaceTriggerCollectiveSupply
|
||||
* (guard 'Offer' 'update:own'). Нажатие = акцепт всего накопленного пула:
|
||||
* сервер формирует сводную заявку status=ACCEPTED и принимает заказы разом.
|
||||
* Ошибки backend (пустой пул, не collective, не ACTIVE, чужой Offer)
|
||||
* приходят как GraphQL-исключения — пробрасываем их вызывающему компоненту.
|
||||
*/
|
||||
export async function triggerCollectiveSupply(offer_id: string): Promise<void> {
|
||||
await client.Mutation(Mutations.Marketplace.TriggerCollectiveSupply.mutation, {
|
||||
variables: { input: { offer_id } },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
/**
|
||||
* Story 4.7: типы формы публикации Offer'а с cycle_type.
|
||||
* Эпик 15: типы формы публикации Offer'а. Тип поставки упразднён — управление
|
||||
* поставкой сведено к набору КУ с минимальным объёмом на каждом
|
||||
* (`delivery_points`). min_supply_volume = 1 → поставка по одному заказу,
|
||||
* >1 → накопление партии (мягкий ориентир, не порог).
|
||||
*
|
||||
* Источник истины — backend `MarketplaceCreateOfferInputDTO`. Ручная
|
||||
* типизация — техдолг до регенерации Zeus в `@coopenomics/sdk` (как
|
||||
* на других страницах Marketplace).
|
||||
*
|
||||
* Способ поставки (ревизия L11, 2026-05-31) — два режима:
|
||||
* individual — каждый заказ обрабатывается отдельно;
|
||||
* collective — заказы копятся в пул (авто-старт по target_volume либо
|
||||
* ручной запуск поставщиком).
|
||||
* Источник истины — backend `MarketplaceCreateOfferInputDTO`.
|
||||
*/
|
||||
export type MarketplaceOfferCycleType = 'individual' | 'collective';
|
||||
export interface MarketplaceOfferDeliveryPoint {
|
||||
/** Кооперативный участок (ПВЗ), на который поставщик готов везти. */
|
||||
braname: string;
|
||||
/** Минимальный объём поставки на этот участок (в единицах товара, ≥ 1). */
|
||||
min_supply_volume: number;
|
||||
}
|
||||
|
||||
export type MarketplaceUnitOfMeasure = 'piece' | 'kg' | 'liter' | 'pack';
|
||||
|
||||
@@ -54,8 +55,7 @@ export interface MarketplaceCreateOfferFormState {
|
||||
unit_of_measure: MarketplaceUnitOfMeasure;
|
||||
quantity_available: number | null;
|
||||
unlimited_flag: boolean;
|
||||
cycle_type: MarketplaceOfferCycleType;
|
||||
target_volume: number | null;
|
||||
delivery_points: MarketplaceOfferDeliveryPoint[];
|
||||
warranty_days: number;
|
||||
}
|
||||
|
||||
@@ -67,8 +67,7 @@ export interface MarketplaceCreateOfferPayload {
|
||||
unit_of_measure: MarketplaceUnitOfMeasure;
|
||||
quantity_available: number | null;
|
||||
unlimited_flag: boolean;
|
||||
cycle_type: MarketplaceOfferCycleType;
|
||||
target_volume: number | null;
|
||||
delivery_points: MarketplaceOfferDeliveryPoint[];
|
||||
warranty_days: number;
|
||||
/**
|
||||
* Итоговый упорядоченный набор изображений (первый — обложка). При создании —
|
||||
@@ -89,7 +88,6 @@ export interface MarketplaceCreateOfferResult {
|
||||
id: string;
|
||||
status: 'PENDING_MODERATION' | 'ACTIVE' | 'REJECTED' | 'WITHDRAWN';
|
||||
product_name: string;
|
||||
cycle_type: MarketplaceOfferCycleType;
|
||||
}
|
||||
|
||||
export interface MarketplaceUpdateOfferPayload extends MarketplaceCreateOfferPayload {
|
||||
@@ -109,8 +107,7 @@ export interface MarketplaceOfferEditPrefill {
|
||||
unit_of_measure: string;
|
||||
quantity_available: number;
|
||||
unlimited_flag: boolean;
|
||||
cycle_type: MarketplaceOfferCycleType;
|
||||
target_volume: number | null;
|
||||
delivery_points: MarketplaceOfferDeliveryPoint[];
|
||||
warranty_days: number;
|
||||
status: 'PENDING_MODERATION' | 'ACTIVE' | 'REJECTED' | 'WITHDRAWN';
|
||||
reject_reason?: string | null;
|
||||
|
||||
+101
-111
@@ -3,18 +3,10 @@ q-page.mp-role-offerer.offer-wizard(role='region', aria-label='Создание
|
||||
.offer-wizard__col
|
||||
//- Заголовок страницы — в топбаре (route.meta.title), на странице не
|
||||
//- дублируется. Верхняя строка режима редактирования: статус слева,
|
||||
//- операционные действия (снять / запустить поставку) — в правом углу.
|
||||
//- операционное действие (снять с публикации) — в правом углу.
|
||||
.offer-wizard__manage(v-if='isEdit && currentStatus')
|
||||
BaseChip(:variant='statusVariant', size='lg') {{ statusLabel }}
|
||||
q-space
|
||||
BaseButton(
|
||||
v-if='canTrigger',
|
||||
variant='secondary',
|
||||
:loading='triggering',
|
||||
@click='onTrigger'
|
||||
)
|
||||
q-icon(name='local_shipping', size='16px')
|
||||
span.q-ml-sm Запустить поставку
|
||||
BaseButton(
|
||||
v-if='canWithdraw',
|
||||
variant='danger',
|
||||
@@ -155,34 +147,29 @@ q-page.mp-role-offerer.offer-wizard(role='region', aria-label='Создание
|
||||
:rules='[(v) => (v !== null && v >= 0) || "Срок гарантии не может быть отрицательным"]'
|
||||
)
|
||||
|
||||
//- ───────── Шаг 3: Условия поставки ─────────
|
||||
//- ───────── Шаг 3: КУ поставки и минимальный объём ─────────
|
||||
.offer-wizard__step(v-else-if='step.key === "supply"')
|
||||
.offer-wizard__cards
|
||||
BaseRadioCard(
|
||||
v-for='opt in cycleTypeOptions',
|
||||
:key='opt.value',
|
||||
:model-value='form.cycle_type',
|
||||
:value='opt.value',
|
||||
:title='opt.title',
|
||||
:description='opt.description',
|
||||
@update:model-value='onSelectCycle(opt.value)'
|
||||
p.offer-wizard__hint
|
||||
| Отметьте кооперативные участки, на которые готовы везти, и укажите минимальный объём поставки на каждый. Объём 1 — поставка по одному заказу; больше — заказы копятся в партию (ориентир для группировки, не жёсткий порог: можно принять и меньше).
|
||||
.offer-wizard__hint(v-if='kuLoading') Загрузка участков…
|
||||
.offer-wizard__hint(v-else-if='!kuOptions.length') Нет доступных кооперативных участков.
|
||||
.offer-wizard__ku-row(v-for='ku in kuOptions', :key='ku.braname')
|
||||
BaseCheckbox(
|
||||
:model-value='isKuSelected(ku.braname)',
|
||||
:label='ku.label',
|
||||
@update:model-value='(v) => toggleKu(ku.braname, v)'
|
||||
)
|
||||
|
||||
//- conditional поля по способу поставки
|
||||
.offer-wizard__conditional(v-if='isCollective')
|
||||
q-input(
|
||||
v-model.number='form.target_volume',
|
||||
label='Целевой объём для авто-старта (необязательно)',
|
||||
q-input.offer-wizard__ku-min(
|
||||
v-if='isKuSelected(ku.braname)',
|
||||
:model-value='kuMinVolume(ku.braname)',
|
||||
label='Мин. объём',
|
||||
type='number',
|
||||
min='1',
|
||||
outlined,
|
||||
dense,
|
||||
no-error-icon,
|
||||
reserve-hint-space,
|
||||
hint='Когда сумма заказов достигнет объёма — поставка стартует автоматически. Пусто — запускаете поставку вручную.'
|
||||
@update:model-value='(v) => setKuMin(ku.braname, v)'
|
||||
)
|
||||
.offer-wizard__hint(v-else)
|
||||
| Каждый заказ принимается индивидуально, без ожидания набора партии.
|
||||
|
||||
//- ───────── Шаг 4: Изображения ─────────
|
||||
.offer-wizard__step(v-else-if='step.key === "images"')
|
||||
@@ -268,12 +255,9 @@ q-page.mp-role-offerer.offer-wizard(role='region', aria-label='Создание
|
||||
section.offer-preview__specs
|
||||
.offer-preview__specs-title Характеристики
|
||||
dl.offer-preview__specs-list
|
||||
.offer-preview__spec
|
||||
dt Способ поставки
|
||||
dd {{ selectedCycleTitle }}
|
||||
.offer-preview__spec(v-if='isCollective && form.target_volume')
|
||||
dt Целевой объём
|
||||
dd {{ form.target_volume }} {{ selectedUnitLabel }}
|
||||
.offer-preview__spec(v-if='form.delivery_points.length')
|
||||
dt Участки поставки
|
||||
dd {{ deliveryPointsPreview }}
|
||||
.offer-preview__spec(v-if='form.warranty_days > 0')
|
||||
dt Гарантия
|
||||
dd {{ form.warranty_days }} дн.
|
||||
@@ -306,10 +290,11 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { VerticalStepper } from 'src/shared/ui/domain/VerticalStepper';
|
||||
import type { StepperStep } from 'src/shared/ui/domain/VerticalStepper';
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton';
|
||||
import { BaseRadioCard } from 'src/shared/ui/base/BaseRadioCard';
|
||||
import { BaseCheckbox } from 'src/shared/ui/base/BaseCheckbox';
|
||||
import { BaseChip } from 'src/shared/ui/base/BaseChip';
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api';
|
||||
import { Queries } from '@coopenomics/sdk';
|
||||
import { client } from 'src/shared/api/client';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { MARKETPLACE_UNIT_OPTIONS } from 'src/shared/lib/consts';
|
||||
import { fileToBase64, formatAsset2Digits } from 'src/shared/lib/utils';
|
||||
@@ -317,7 +302,6 @@ import {
|
||||
createOffer,
|
||||
fetchCategories,
|
||||
fetchMyOfferById,
|
||||
triggerCollectiveSupply,
|
||||
updateOffer,
|
||||
withdrawOffer,
|
||||
} from '../api';
|
||||
@@ -325,7 +309,6 @@ import type {
|
||||
MarketplaceCategoryView,
|
||||
MarketplaceCreateOfferFormState,
|
||||
MarketplaceCreateOfferPayload,
|
||||
MarketplaceOfferCycleType,
|
||||
MarketplaceOfferImageUpload,
|
||||
MarketplaceUnitOfMeasure,
|
||||
} from '../types';
|
||||
@@ -400,10 +383,8 @@ const submitLabel = computed(() => {
|
||||
// живут на самой странице редактирования — отдельного диалога-просмотра нет.
|
||||
type OfferStatus = 'PENDING_MODERATION' | 'ACTIVE' | 'REJECTED' | 'WITHDRAWN';
|
||||
const currentStatus = ref<OfferStatus | null>(null);
|
||||
const currentCycleType = ref<MarketplaceOfferCycleType | null>(null);
|
||||
const rejectReason = ref<string | null>(null);
|
||||
const withdrawing = ref(false);
|
||||
const triggering = ref(false);
|
||||
|
||||
const STATUS_META: Record<OfferStatus, { label: string; variant: 'neutral' | 'pos' | 'neg' | 'warn' }> = {
|
||||
PENDING_MODERATION: { label: 'На модерации', variant: 'warn' },
|
||||
@@ -415,14 +396,10 @@ const statusLabel = computed(() => (currentStatus.value ? STATUS_META[currentSta
|
||||
const statusVariant = computed(() =>
|
||||
currentStatus.value ? STATUS_META[currentStatus.value].variant : 'neutral'
|
||||
);
|
||||
// Снять можно опубликованную или ожидающую модерации; запуск поставки вручную —
|
||||
// только активную оферту с коллективной закупкой (см. backend guard'ы).
|
||||
// Снять можно опубликованную или ожидающую модерации.
|
||||
const canWithdraw = computed(
|
||||
() => currentStatus.value === 'PENDING_MODERATION' || currentStatus.value === 'ACTIVE'
|
||||
);
|
||||
const canTrigger = computed(
|
||||
() => currentStatus.value === 'ACTIVE' && currentCycleType.value === 'collective'
|
||||
);
|
||||
|
||||
function onWithdraw(): void {
|
||||
if (!editId.value) return;
|
||||
@@ -448,30 +425,6 @@ function onWithdraw(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function onTrigger(): void {
|
||||
if (!editId.value) return;
|
||||
Dialog.create({
|
||||
title: 'Запустить поставку?',
|
||||
message:
|
||||
'Все накопленные заказы будут разом зафиксированы в одну партию и приняты ' +
|
||||
'к поставке. Отменить запуск нельзя.',
|
||||
cancel: { label: 'Отмена', flat: true, noCaps: true },
|
||||
ok: { label: 'Запустить поставку', color: 'primary', unelevated: true, noCaps: true },
|
||||
persistent: true,
|
||||
}).onOk(async () => {
|
||||
triggering.value = true;
|
||||
try {
|
||||
await triggerCollectiveSupply(editId.value as string);
|
||||
SuccessAlert('Поставка запущена: заказы зафиксированы в партию и приняты.');
|
||||
void router.push({ name: 'marketplace-my-offers' });
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось запустить поставку');
|
||||
} finally {
|
||||
triggering.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Шаги =====
|
||||
const steps: StepperStep[] = [
|
||||
{ key: 'basics', label: 'Товар', description: 'Название, категория, описание' },
|
||||
@@ -492,23 +445,15 @@ const pricingForm = ref<QForm | null>(null);
|
||||
// подписи не расходились между созданием оферты, модерацией и «Мои предложения».
|
||||
const unitOptions = MARKETPLACE_UNIT_OPTIONS;
|
||||
|
||||
const cycleTypeOptions: Array<{
|
||||
value: MarketplaceOfferCycleType;
|
||||
title: string;
|
||||
description: string;
|
||||
}> = [
|
||||
{
|
||||
value: 'individual',
|
||||
title: 'Индивидуально',
|
||||
description: 'Каждый заказ обрабатывается отдельно, без ожидания набора партии.',
|
||||
},
|
||||
{
|
||||
value: 'collective',
|
||||
title: 'Коллективная закупка',
|
||||
description:
|
||||
'Заказы копятся в общий пул. Старт — автоматически по целевому объёму либо вручную вами.',
|
||||
},
|
||||
];
|
||||
// ===== КУ поставки (Эпик 15) =====
|
||||
// Список кооперативных участков кооператива для чекбоксов: поставщик отмечает,
|
||||
// на какие КУ готов везти, и ставит min-объём на каждый.
|
||||
interface KuOption {
|
||||
braname: string;
|
||||
label: string;
|
||||
}
|
||||
const kuOptions = ref<KuOption[]>([]);
|
||||
const kuLoading = ref(false);
|
||||
|
||||
const categories = ref<MarketplaceCategoryView[]>([]);
|
||||
const categoryOptions = computed(() =>
|
||||
@@ -527,8 +472,7 @@ const form = ref<MarketplaceCreateOfferFormState>({
|
||||
unit_of_measure: 'piece',
|
||||
quantity_available: 1,
|
||||
unlimited_flag: false,
|
||||
cycle_type: 'individual',
|
||||
target_volume: null,
|
||||
delivery_points: [],
|
||||
warranty_days: 0,
|
||||
});
|
||||
|
||||
@@ -556,18 +500,71 @@ const previewActive = ref(0);
|
||||
const originalImageKeys = ref<string[]>([]);
|
||||
let galleryUidSeq = 0;
|
||||
|
||||
const isCollective = computed(() => form.value.cycle_type === 'collective');
|
||||
|
||||
const selectedCategoryLabel = computed(
|
||||
() => categoryOptions.value.find((o) => o.value === form.value.category_id)?.label ?? '—'
|
||||
);
|
||||
const selectedUnitLabel = computed(
|
||||
() => unitOptions.find((o) => o.value === form.value.unit_of_measure)?.label ?? ''
|
||||
);
|
||||
const selectedCycleTitle = computed(
|
||||
() => cycleTypeOptions.find((o) => o.value === form.value.cycle_type)?.title ?? '—'
|
||||
const deliveryPointsPreview = computed(() =>
|
||||
form.value.delivery_points
|
||||
.map((d) => {
|
||||
const label = kuOptions.value.find((k) => k.braname === d.braname)?.label ?? d.braname;
|
||||
return `${label} (от ${d.min_supply_volume})`;
|
||||
})
|
||||
.join(', ')
|
||||
);
|
||||
|
||||
// ===== КУ-хелперы =====
|
||||
function isKuSelected(braname: string): boolean {
|
||||
return form.value.delivery_points.some((d) => d.braname === braname);
|
||||
}
|
||||
function toggleKu(braname: string, checked: boolean): void {
|
||||
if (checked) {
|
||||
if (!isKuSelected(braname)) {
|
||||
form.value.delivery_points = [
|
||||
...form.value.delivery_points,
|
||||
{ braname, min_supply_volume: 1 },
|
||||
];
|
||||
}
|
||||
} else {
|
||||
form.value.delivery_points = form.value.delivery_points.filter((d) => d.braname !== braname);
|
||||
}
|
||||
}
|
||||
function kuMinVolume(braname: string): number {
|
||||
return form.value.delivery_points.find((d) => d.braname === braname)?.min_supply_volume ?? 1;
|
||||
}
|
||||
function setKuMin(braname: string, value: string | number | null): void {
|
||||
const n = Math.max(1, Math.floor(Number(value) || 1));
|
||||
form.value.delivery_points = form.value.delivery_points.map((d) =>
|
||||
d.braname === braname ? { ...d, min_supply_volume: n } : d
|
||||
);
|
||||
}
|
||||
|
||||
async function loadKuOptions(): Promise<void> {
|
||||
kuLoading.value = true;
|
||||
try {
|
||||
const result = await client.Query(Queries.Marketplace.ListKUDetails.query, {
|
||||
variables: { data: {} },
|
||||
});
|
||||
const list = (result[Queries.Marketplace.ListKUDetails.name] ?? []) as Array<{
|
||||
coreBraname: string;
|
||||
addressFull?: string | null;
|
||||
status?: string | null;
|
||||
}>;
|
||||
kuOptions.value = list
|
||||
.filter((k) => !k.status || k.status === 'active' || k.status === 'ACTIVE')
|
||||
.map((k) => ({
|
||||
braname: k.coreBraname,
|
||||
label: k.addressFull ? `${k.coreBraname} — ${k.addressFull}` : k.coreBraname,
|
||||
}));
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить кооперативные участки');
|
||||
} finally {
|
||||
kuLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Нормализованная (точка-разделитель) цена для отправки и форматирования.
|
||||
const priceNumberStr = computed(() => form.value.price_per_unit.trim().replace(',', '.'));
|
||||
const formattedPrice = computed(() =>
|
||||
@@ -690,15 +687,6 @@ function setCover(index: number): void {
|
||||
coverIndex.value = index;
|
||||
}
|
||||
|
||||
function onSelectCycle(value: MarketplaceOfferCycleType): void {
|
||||
form.value.cycle_type = value;
|
||||
// target_volume имеет смысл только для коллективной закупки (опциональный
|
||||
// авто-старт). У индивидуальной поставки его нет.
|
||||
if (value === 'individual') {
|
||||
form.value.target_volume = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function onPickFiles(files: readonly File[] | null): Promise<void> {
|
||||
const list = files ? [...files] : [];
|
||||
// Берём только ещё не добавленные файлы (по имени) — на случай повторного выбора.
|
||||
@@ -751,11 +739,12 @@ function markCompleted(key: string): void {
|
||||
}
|
||||
|
||||
function validateSupply(): string | null {
|
||||
const f = form.value;
|
||||
// Целевой объём для коллективной закупки необязателен (пусто = ручной запуск),
|
||||
// но если задан — должен быть положительным.
|
||||
if (f.cycle_type === 'collective' && f.target_volume != null && f.target_volume < 1) {
|
||||
return 'Целевой объём должен быть от 1 (или оставьте пустым для ручного запуска).';
|
||||
const points = form.value.delivery_points;
|
||||
if (!points.length) {
|
||||
return 'Отметьте хотя бы один кооперативный участок поставки.';
|
||||
}
|
||||
if (points.some((d) => !Number.isInteger(d.min_supply_volume) || d.min_supply_volume < 1)) {
|
||||
return 'Минимальный объём на каждом участке должен быть целым числом от 1.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -835,8 +824,7 @@ async function onSubmit(): Promise<void> {
|
||||
unit_of_measure: f.unit_of_measure,
|
||||
quantity_available: f.unlimited_flag ? null : f.quantity_available,
|
||||
unlimited_flag: f.unlimited_flag,
|
||||
cycle_type: f.cycle_type,
|
||||
target_volume: f.cycle_type === 'collective' ? f.target_volume : null,
|
||||
delivery_points: f.delivery_points,
|
||||
warranty_days: f.warranty_days,
|
||||
images: buildImagesPayload(),
|
||||
};
|
||||
@@ -885,8 +873,10 @@ async function prefillForEdit(id: string): Promise<void> {
|
||||
unit_of_measure: offer.unit_of_measure as MarketplaceUnitOfMeasure,
|
||||
quantity_available: offer.quantity_available,
|
||||
unlimited_flag: offer.unlimited_flag,
|
||||
cycle_type: offer.cycle_type,
|
||||
target_volume: offer.target_volume,
|
||||
delivery_points: (offer.delivery_points ?? []).map((d) => ({
|
||||
braname: d.braname,
|
||||
min_supply_volume: d.min_supply_volume,
|
||||
})),
|
||||
warranty_days: offer.warranty_days,
|
||||
};
|
||||
gallery.value = (offer.images ?? [])
|
||||
@@ -901,7 +891,6 @@ async function prefillForEdit(id: string): Promise<void> {
|
||||
originalImageKeys.value = gallery.value.map((g) => g.bucket_key as string);
|
||||
coverIndex.value = 0;
|
||||
currentStatus.value = offer.status;
|
||||
currentCycleType.value = offer.cycle_type;
|
||||
rejectReason.value = offer.reject_reason ?? null;
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить предложение');
|
||||
@@ -916,6 +905,7 @@ onMounted(async () => {
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить категории');
|
||||
}
|
||||
await loadKuOptions();
|
||||
if (editId.value) {
|
||||
await prefillForEdit(editId.value);
|
||||
} else {
|
||||
|
||||
@@ -29,5 +29,3 @@ export type MarketplaceOrderPage = Omit<_RawOrderPage, 'items'> & {
|
||||
* самих заказов. Никаких параллельных строковых union'ов.
|
||||
*/
|
||||
export type MarketplaceOrderStatusView = `${MarketplaceOrderView['status']}`;
|
||||
|
||||
export type MarketplaceOrderCycleTypeView = `${MarketplaceOrderView['cycle_type']}`;
|
||||
|
||||
@@ -50,30 +50,23 @@ export async function fetchSupplierOrders(
|
||||
return result as MarketplaceOrderPage;
|
||||
}
|
||||
|
||||
export async function acceptIndividualOrder(order_id: string): Promise<void> {
|
||||
await client.Mutation(Mutations.Marketplace.AcceptIndividualOrder.mutation, {
|
||||
variables: { input: { order_id } },
|
||||
});
|
||||
}
|
||||
|
||||
export async function declineIndividualOrder(order_id: string, reason: string): Promise<void> {
|
||||
await client.Mutation(Mutations.Marketplace.DeclineIndividualOrder.mutation, {
|
||||
variables: { input: { order_id, reason } },
|
||||
/**
|
||||
* Эпик 15: поставщик принимает к поставке выбранные заказы (любое подмножество
|
||||
* группы offer × КУ) единым массивом. Backend оборачивает их в одну
|
||||
* партию-накопитель и акцептует on-chain.
|
||||
*/
|
||||
export async function acceptOrdersBatch(order_ids: string[]): Promise<void> {
|
||||
await client.Mutation(Mutations.Marketplace.AcceptOrdersBatch.mutation, {
|
||||
variables: { input: { order_ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Групповой (collective) акцепт: поставщик принимает всю сводную заявку
|
||||
* партии разом. request_id = cycle_id заказа.
|
||||
* Эпик 15: поставщик отказывается от выбранных активных заказов массивом;
|
||||
* средства пайщиков разблокируются.
|
||||
*/
|
||||
export async function acceptConsolidatedRequest(request_id: string): Promise<void> {
|
||||
await client.Mutation(Mutations.Marketplace.AcceptConsolidatedRequest.mutation, {
|
||||
variables: { input: { request_id } },
|
||||
});
|
||||
}
|
||||
|
||||
export async function declineConsolidatedRequest(request_id: string, reason: string): Promise<void> {
|
||||
await client.Mutation(Mutations.Marketplace.DeclineConsolidatedRequest.mutation, {
|
||||
variables: { input: { request_id, reason } },
|
||||
export async function declineOrdersBatch(order_ids: string[], reason: string): Promise<void> {
|
||||
await client.Mutation(Mutations.Marketplace.DeclineOrdersBatch.mutation, {
|
||||
variables: { input: { order_ids, reason } },
|
||||
});
|
||||
}
|
||||
|
||||
+8
-30
@@ -13,10 +13,8 @@ import {
|
||||
} from 'src/widgets/Marketplace/OrderCard';
|
||||
import { RefreshButton } from 'src/widgets/Marketplace/RefreshButton';
|
||||
import {
|
||||
acceptConsolidatedRequest,
|
||||
acceptIndividualOrder,
|
||||
declineConsolidatedRequest,
|
||||
declineIndividualOrder,
|
||||
acceptOrdersBatch,
|
||||
declineOrdersBatch,
|
||||
fetchSupplierOrders,
|
||||
} from '../api';
|
||||
import type {
|
||||
@@ -139,21 +137,10 @@ function loadMore(): void {
|
||||
async function onAccept(orderId: string): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
// Индивидуальный заказ акцептуется поштучно (order_id); групповой
|
||||
// (cycle_type collective) — целиком сводной заявкой по cycle_id. Иначе
|
||||
// acceptIndividualOrder на групповом заказе падает: заказ привязан к
|
||||
// сводной заявке, а не к индивидуальному циклу.
|
||||
const order = items.value.find((o) => o.id === orderId);
|
||||
if (order && order.status === 'ACCEPTED_PENDING_SUPPLIER') {
|
||||
if (!order.cycle_id) {
|
||||
throw new Error('У группового заказа отсутствует идентификатор сводной заявки (cycle_id).');
|
||||
}
|
||||
await acceptConsolidatedRequest(order.cycle_id);
|
||||
SuccessAlert('Сводная заявка партии принята');
|
||||
} else {
|
||||
await acceptIndividualOrder(orderId);
|
||||
SuccessAlert('Заказ принят');
|
||||
}
|
||||
// Эпик 15: единый batch-accept по order_id[]. Здесь принимаем по одному
|
||||
// (партия из одного); мультивыбор группой (offer × КУ) — фоллоуап UI.
|
||||
await acceptOrdersBatch([orderId]);
|
||||
SuccessAlert('Заказ принят к поставке');
|
||||
await load(1, false);
|
||||
} catch (e) {
|
||||
FailAlert(e);
|
||||
@@ -173,17 +160,8 @@ function onDecline(orderId: string): void {
|
||||
}).onOk(async (reason: string) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const order = items.value.find((o) => o.id === orderId);
|
||||
if (order && order.status === 'ACCEPTED_PENDING_SUPPLIER') {
|
||||
if (!order.cycle_id) {
|
||||
throw new Error('У группового заказа отсутствует идентификатор сводной заявки (cycle_id).');
|
||||
}
|
||||
await declineConsolidatedRequest(order.cycle_id, reason.trim());
|
||||
NotifyAlert('Сводная заявка партии отклонена');
|
||||
} else {
|
||||
await declineIndividualOrder(orderId, reason.trim());
|
||||
NotifyAlert('Заказ отклонён');
|
||||
}
|
||||
await declineOrdersBatch([orderId], reason.trim());
|
||||
NotifyAlert('Заказ отклонён');
|
||||
await load(1, false);
|
||||
} catch (e) {
|
||||
FailAlert(e);
|
||||
|
||||
@@ -17,5 +17,3 @@ export type MarketplaceOfferView = MarketplaceOfferPage['items'][number];
|
||||
* самих предложений.
|
||||
*/
|
||||
export type MarketplaceOfferStatusView = `${MarketplaceOfferView['status']}`;
|
||||
|
||||
export type MarketplaceOfferCycleTypeView = `${MarketplaceOfferView['cycle_type']}`;
|
||||
|
||||
+2
-15
@@ -11,7 +11,6 @@ import { EmptyState } from 'src/shared/ui/base';
|
||||
import { PageHint } from 'src/shared/ui/domain';
|
||||
import { fetchMyOrders } from '../../MyOrders/api';
|
||||
import type {
|
||||
MarketplaceOrderCycleTypeView,
|
||||
MarketplaceOrderStatusView,
|
||||
MarketplaceOrderView,
|
||||
} from '../../MyOrders/types';
|
||||
@@ -36,17 +35,6 @@ const items = ref<MarketplaceOrderView[]>([]);
|
||||
const loading = ref(false);
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const CYCLE_TYPE_LABEL: Record<MarketplaceOrderCycleTypeView, string> = {
|
||||
COLLECTIVE: 'Коллективная закупка',
|
||||
INDIVIDUAL: 'Индивидуальный',
|
||||
};
|
||||
|
||||
const CYCLE_TYPE_HINT: Record<MarketplaceOrderCycleTypeView, string> = {
|
||||
COLLECTIVE:
|
||||
'Заказы копятся в общий пул; поставка стартует по достижении целевого объёма либо по решению поставщика.',
|
||||
INDIVIDUAL: 'Каждый заказ обслуживается поставщиком отдельно.',
|
||||
};
|
||||
|
||||
// Этап партии = минимальный по STAGE_RANK среди не-отменённых; то есть
|
||||
// если хотя бы один заказ ещё ждёт цикл, вся партия «Активна».
|
||||
// Готовый к выдаче переходит в issued только когда все заказы выданы.
|
||||
@@ -67,7 +55,6 @@ const STAGE_RANK: Record<MarketplaceOrderStatusView, number> = {
|
||||
interface ConsolidatedGroup {
|
||||
key: string;
|
||||
cycle_id: string | null;
|
||||
cycle_type: MarketplaceOrderCycleTypeView;
|
||||
offer_id: string;
|
||||
supplier_account: string;
|
||||
delivery_braname: string;
|
||||
@@ -88,7 +75,6 @@ const groups = computed<ConsolidatedGroup[]>(() => {
|
||||
g = {
|
||||
key,
|
||||
cycle_id: o.cycle_id ?? null,
|
||||
cycle_type: o.cycle_type,
|
||||
offer_id: o.offer_id,
|
||||
supplier_account: o.supplier_account,
|
||||
delivery_braname: o.delivery_braname,
|
||||
@@ -192,7 +178,8 @@ q-page.consolidated(role="region", aria-label="Сводный заказ")
|
||||
.t-h3
|
||||
span(v-if="g.cycle_id") Партия № {{ g.cycle_id }}
|
||||
span(v-else) Индивидуальный заказ № {{ g.orders[0].id.slice(0, 8) }}
|
||||
.t-muted.consolidated__group-hint {{ CYCLE_TYPE_LABEL[g.cycle_type] }} — {{ CYCLE_TYPE_HINT[g.cycle_type] }}
|
||||
.t-muted.consolidated__group-hint(v-if="g.cycle_id") Партия-накопитель — заказы приняты поставщиком к поставке вместе.
|
||||
.t-muted.consolidated__group-hint(v-else) Отдельный заказ — ещё не принят поставщиком к поставке.
|
||||
q-space
|
||||
span.chip.chip--accent
|
||||
q-icon(name="layers", size="14px")
|
||||
|
||||
@@ -44,20 +44,3 @@ export function marketplaceUnitLabel(value: string | null | undefined): string {
|
||||
if (!value) return 'ед.';
|
||||
return MARKETPLACE_UNITS.find((u) => u.value === value)?.label ?? value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Русские подписи способа поставки (cycle_type) — единый источник для всех
|
||||
* marketplace-экранов (модерация, каталог, создание оферты). Раньше карта
|
||||
* дублировалась по нескольким страницам (создание оферты, сводный заказ).
|
||||
* Значения совпадают с backend-enum `MarketplaceOfferCycleType` (два режима).
|
||||
*/
|
||||
const MARKETPLACE_CYCLE_LABELS: Record<string, string> = {
|
||||
individual: 'Индивидуально',
|
||||
collective: 'Коллективная закупка',
|
||||
};
|
||||
|
||||
/** Русская подпись способа поставки; неизвестное значение возвращается как есть. */
|
||||
export function marketplaceCycleLabel(value: string | null | undefined): string {
|
||||
if (!value) return '';
|
||||
return MARKETPLACE_CYCLE_LABELS[value] ?? value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user