fix(marketplace): код получения — диалог из шапки + единый виджет карточки партии
- «Получить заказ» вернул из переадресации в диалог (ReceiveCodeDialog), кнопка в шапке и на «Моих заказах», и на детали заказа — в одном месте; отдельная страница меню остаётся. Общий ReceiveCodeContent (DRY). - OfferGallery: точки-навигация выключены по умолчанию (убраны со всех экранов). - Вынес SupplyPartyCard — единая карточка партии для заказчика («Коллективный заказ») и поставщика («Входящие заказы»), вместо дублей. - Подчистил мёртвые стили карусели/плейсхолдера в CatalogOfferCard и странице оферты. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
-7
@@ -201,13 +201,6 @@ q-page.offer-detail(role="region", aria-label="Описание предложе
|
||||
background: var(--p-surface-2);
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
height: 360px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RefreshButton } from 'src/widgets/Marketplace/RefreshButton';
|
||||
import { BaseButton, EmptyState } from 'src/shared/ui/base';
|
||||
import { PageHint } from 'src/shared/ui/domain';
|
||||
import { PageTabs, type PageTab } from 'src/shared/ui/layout';
|
||||
import { ReceiveCodeDialog } from 'src/widgets/Marketplace/ReceiveCode';
|
||||
import { cancelOrder, fetchMyOrders } from '../api';
|
||||
import type { MarketplaceOrderStatusView, MarketplaceOrderView } from '../types';
|
||||
import OrdererFinalizeIssuanceDialog from './OrdererFinalizeIssuanceDialog.vue';
|
||||
@@ -21,8 +22,8 @@ import OrdererFinalizeIssuanceDialog from './OrdererFinalizeIssuanceDialog.vue';
|
||||
* выдачу, статус READY_TO_RECEIVE) — отдельной страницы «Готово к получению»
|
||||
* больше нет. Клик по карточке открывает детальную страницу заказа.
|
||||
*
|
||||
* Код получения (account-bound QR) — на отдельной странице меню «Получить
|
||||
* заказ» (OrdererReceiveCode), а не здесь: так он очевидно findable.
|
||||
* Код получения (account-bound QR) — кнопка «Получить заказ» в шапке: открывает
|
||||
* диалог с тем же QR, что и на отдельной странице меню (OrdererReceiveCode).
|
||||
*
|
||||
* Live-обновления — polling каждые 10s.
|
||||
*/
|
||||
@@ -47,6 +48,9 @@ const hasMore = computed(() => currentPage.value < totalPages.value);
|
||||
const finalizeDialogOpen = ref(false);
|
||||
const selectedOrder = ref<MarketplaceOrderView | null>(null);
|
||||
|
||||
// Код получения (account-bound QR) — диалогом из шапки, в одном месте.
|
||||
const receiveDialogOpen = ref(false);
|
||||
|
||||
// Фильтр по этапу. Покрытие ИСЧЕРПЫВАЮЩЕЕ по enum'у MarketplaceOrderStatusView:
|
||||
// каждый статус заказа попадает хотя бы в одну вкладку. Иначе заказ молча
|
||||
// «растекается» — пропадает из всех вкладок, кроме «Все» (баг: молоко в статусе
|
||||
@@ -160,7 +164,7 @@ function openDetail(order: OrderCardModel): void {
|
||||
}
|
||||
|
||||
function goReceive(): void {
|
||||
void router.push({ name: 'marketplace-receive-code', params: { coopname: coopname.value } });
|
||||
receiveDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function onCardAction(payload: { key: string; order: OrderCardModel }): void {
|
||||
@@ -230,6 +234,8 @@ q-page.orders(role="region", aria-label="Мои заказы")
|
||||
:order="selectedOrder",
|
||||
@finalized="onFinalized"
|
||||
)
|
||||
|
||||
ReceiveCodeDialog(v-model="receiveDialogOpen", :coopname="coopname")
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
+37
-145
@@ -3,10 +3,11 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Dialog } from 'quasar';
|
||||
import { SuccessAlert, FailAlert, NotifyAlert } from 'src/shared/api';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { BaseBadge, BaseButton, EmptyState } from 'src/shared/ui/base';
|
||||
import { BaseButton, EmptyState } from 'src/shared/ui/base';
|
||||
import { PageHint } from 'src/shared/ui/domain';
|
||||
import { PageTabs, type PageTab } from 'src/shared/ui/layout';
|
||||
import { orderStatusDisplay } from 'src/widgets/Marketplace/OrderCard';
|
||||
import { SupplyPartyCard } from 'src/widgets/Marketplace/SupplyPartyCard';
|
||||
import { RefreshButton } from 'src/widgets/Marketplace/RefreshButton';
|
||||
import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
|
||||
import { formatShortFio } from 'src/shared/lib/utils/getNameFromCertificate';
|
||||
@@ -203,6 +204,17 @@ function formatCost(value: number): string {
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
// Состав партии строками для общего виджета SupplyPartyCard. Поставщику важен
|
||||
// общий объём, не кто заказал — имена показываем справочно, без дробления.
|
||||
function memberRows(p: SupplierParty): Array<{ id: string; who: string; qty: string; cost: string }> {
|
||||
return p.orders.map((o) => ({
|
||||
id: o.id,
|
||||
who: o.orderer_name ? formatShortFio(o.orderer_name) : o.orderer_account,
|
||||
qty: `${o.quantity} ${p.unitLabel}`,
|
||||
cost: formatCost(parseFloat(o.total_cost) || 0),
|
||||
}));
|
||||
}
|
||||
|
||||
async function load(page: number, append: boolean): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -320,61 +332,35 @@ q-page.incoming-orders(role='region', aria-label='Входящие заказы
|
||||
q-icon(name='inbox', size='48px')
|
||||
|
||||
.incoming-orders__list(v-if='hasParties')
|
||||
.incoming-orders__party(
|
||||
//- ЕДИНАЯ карточка партии (канон-виджет SupplyPartyCard) — та же, что у
|
||||
//- заказчика. Действия (Принять/Отклонить) — только пока партия копится.
|
||||
SupplyPartyCard(
|
||||
v-for='p in parties',
|
||||
:key='p.key',
|
||||
:class='`incoming-orders__party--${p.kind}`'
|
||||
:product-name='p.productName',
|
||||
:pvz-name='p.pvzName',
|
||||
:stage-status='p.stageStatus',
|
||||
:order-count='p.orders.length',
|
||||
:volume-label='`Объём партии: ${p.totalUnits} ${p.unitLabel}`',
|
||||
:target-label='hasTarget(p) ? `цель — от ${p.minVolume} ${p.unitLabel}` : ""',
|
||||
:progress='progressRatio(p)',
|
||||
:bar-color='barColor(p)',
|
||||
:members='memberRows(p)',
|
||||
total-label='Итого партии',
|
||||
:total-value='`${formatCost(p.totalCost)} · ${p.totalUnits} ${p.unitLabel}`'
|
||||
)
|
||||
.incoming-orders__party-head
|
||||
.row.items-center.q-gutter-sm.no-wrap
|
||||
div.col
|
||||
.t-h3 {{ p.productName }}
|
||||
.t-muted.incoming-orders__party-sub
|
||||
q-icon(name='place', size='14px')
|
||||
| КУ «{{ p.pvzName }}»
|
||||
BaseBadge(:variant='orderStatusDisplay(p.stageStatus).variant') {{ orderStatusDisplay(p.stageStatus).label }}
|
||||
span.chip.chip--accent
|
||||
q-icon(name='layers', size='14px')
|
||||
| {{ p.orders.length }} зак.
|
||||
|
||||
//- ЕДИНАЯ карточка партии на всех стадиях: прогресс всегда, состав
|
||||
//- компактными строками, действия — только пока партия копится.
|
||||
.incoming-orders__progress
|
||||
.incoming-orders__progress-row
|
||||
span Объём партии: {{ p.totalUnits }} {{ p.unitLabel }}
|
||||
span.t-muted(v-if='hasTarget(p)') цель — от {{ p.minVolume }} {{ p.unitLabel }}
|
||||
q-linear-progress.incoming-orders__progress-bar(
|
||||
:value='progressRatio(p)',
|
||||
rounded,
|
||||
size='10px',
|
||||
:color='barColor(p)',
|
||||
track-color='grey-3'
|
||||
)
|
||||
.t-muted.incoming-orders__progress-hint(v-if='p.kind === "collecting" && hasTarget(p) && reachedMin(p)')
|
||||
template(#hint)
|
||||
template(v-if='p.kind === "collecting" && hasTarget(p) && reachedMin(p)')
|
||||
q-icon(name='check_circle', size='14px', color='positive')
|
||||
| Минимальный объём набран — можно принимать партию.
|
||||
.t-muted.incoming-orders__progress-hint(v-else-if='p.kind === "collecting" && hasTarget(p)')
|
||||
| Можно принять и сейчас — минимум лишь ориентир сбора.
|
||||
.t-muted.incoming-orders__progress-hint(v-else-if='p.kind === "collecting"')
|
||||
| Поштучный приём — каждый заказ можно принять сразу.
|
||||
.t-muted.incoming-orders__progress-hint(v-else)
|
||||
span Минимальный объём набран — можно принимать партию.
|
||||
template(v-else-if='p.kind === "collecting" && hasTarget(p)')
|
||||
span Можно принять и сейчас — минимум лишь ориентир сбора.
|
||||
template(v-else-if='p.kind === "collecting"')
|
||||
span Поштучный приём — каждый заказ можно принять сразу.
|
||||
template(v-else)
|
||||
q-icon(name='local_shipping', size='14px')
|
||||
| Партия принята. Этап: {{ orderStatusDisplay(p.stageStatus).label }}.
|
||||
|
||||
//- Состав партии — компактные строки (поставщику важен общий объём, не
|
||||
//- кто конкретно заказал; имена показываем справочно, без дробления на
|
||||
//- отдельные карточки).
|
||||
.incoming-orders__members
|
||||
.incoming-orders__member(v-for='o in p.orders', :key='o.id')
|
||||
span.incoming-orders__member-who {{ o.orderer_name ? formatShortFio(o.orderer_name) : o.orderer_account }}
|
||||
span.incoming-orders__member-qty {{ o.quantity }} {{ p.unitLabel }}
|
||||
span.incoming-orders__member-cost {{ formatCost(parseFloat(o.total_cost) || 0) }}
|
||||
|
||||
.incoming-orders__party-foot
|
||||
.incoming-orders__party-total
|
||||
span.t-muted Итого партии
|
||||
span.incoming-orders__party-total-val {{ formatCost(p.totalCost) }} · {{ p.totalUnits }} {{ p.unitLabel }}
|
||||
q-space
|
||||
span Партия принята. Этап: {{ orderStatusDisplay(p.stageStatus).label }}.
|
||||
template(#actions)
|
||||
template(v-if='p.kind === "collecting"')
|
||||
BaseButton(variant='ghost', size='sm', @click='onDeclineParty(p)') Отклонить
|
||||
BaseButton(variant='primary', size='sm', :loading='loading', @click='onAcceptParty(p)')
|
||||
@@ -412,100 +398,6 @@ q-page.incoming-orders(role='region', aria-label='Входящие заказы
|
||||
gap: var(--p-4, 16px);
|
||||
}
|
||||
|
||||
&__party {
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
background: var(--p-surface);
|
||||
padding: var(--p-4, 16px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
&__party-sub {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&__progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__progress-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
font-size: var(--p-fs-body);
|
||||
}
|
||||
|
||||
&__progress-bar {
|
||||
border-radius: var(--p-r-sm, 8px);
|
||||
}
|
||||
|
||||
&__progress-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__members {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-sm, 8px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__member {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: var(--p-4, 16px);
|
||||
align-items: center;
|
||||
padding: var(--p-2, 8px) var(--p-3, 12px);
|
||||
border-top: 1px solid var(--p-line);
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__member-who {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__member-qty {
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
&__member-cost {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__party-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-2, 8px);
|
||||
flex-wrap: wrap;
|
||||
padding-top: var(--p-2, 8px);
|
||||
border-top: 1px solid var(--p-line);
|
||||
}
|
||||
|
||||
&__party-total {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__party-total-val {
|
||||
font-size: var(--p-fs-body);
|
||||
color: var(--p-ink-1);
|
||||
}
|
||||
|
||||
&__skel {
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
|
||||
+37
-134
@@ -3,7 +3,8 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { FailAlert } from 'src/shared/api';
|
||||
import { orderStatusDisplay } from 'src/widgets/Marketplace/OrderCard';
|
||||
import { RefreshButton } from 'src/widgets/Marketplace/RefreshButton';
|
||||
import { BaseBadge, EmptyState } from 'src/shared/ui/base';
|
||||
import { SupplyPartyCard } from 'src/widgets/Marketplace/SupplyPartyCard';
|
||||
import { EmptyState } from 'src/shared/ui/base';
|
||||
import { PageHint } from 'src/shared/ui/domain';
|
||||
import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
|
||||
import { fetchMyOrders } from '../../MyOrders/api';
|
||||
@@ -158,6 +159,17 @@ function formatCost(value: number): string {
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
// Состав партии строками для общего виджета SupplyPartyCard. У заказчика «кто» —
|
||||
// его собственные заказы (товар/КУ уже в шапке карточки).
|
||||
function memberRows(p: CollectiveParty): Array<{ id: string; who: string; qty: string; cost: string }> {
|
||||
return p.orders.map((o) => ({
|
||||
id: o.id,
|
||||
who: `Ваш заказ № ${o.id.slice(0, 8)}`,
|
||||
qty: `${o.quantity} ${p.unitLabel}`,
|
||||
cost: formatCost(parseFloat(o.total_cost) || 0),
|
||||
}));
|
||||
}
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -215,51 +227,32 @@ q-page.collective(role="region", aria-label="Коллективный заказ
|
||||
q-icon(name="inventory_2", size="48px")
|
||||
|
||||
.collective__list(v-if="hasParties")
|
||||
//- ЕДИНАЯ карточка для любой партии (сбор/принята) — отличается только
|
||||
//- заполнением прогресса и бейджем этапа, не вёрсткой.
|
||||
.collective__party(v-for="p in parties", :key="p.key")
|
||||
.collective__party-head
|
||||
.row.items-center.q-gutter-sm.no-wrap
|
||||
div.col
|
||||
.t-h3 {{ p.productName }}
|
||||
.t-muted.collective__party-sub
|
||||
q-icon(name="place", size="14px")
|
||||
| КУ «{{ p.pvzName }}»
|
||||
BaseBadge(:variant="orderStatusDisplay(p.stageStatus).variant") {{ orderStatusDisplay(p.stageStatus).label }}
|
||||
span.chip.chip--accent
|
||||
q-icon(name="layers", size="14px")
|
||||
| {{ p.orders.length }} зак.
|
||||
|
||||
.collective__progress
|
||||
.collective__progress-row
|
||||
span Накоплено всеми: {{ accumulated(p) }} {{ p.unitLabel }}
|
||||
span.t-muted(v-if="hasTarget(p)") цель — от {{ p.groupMinVolume }} {{ p.unitLabel }}
|
||||
q-linear-progress.collective__progress-bar(
|
||||
:value="progressRatio(p)",
|
||||
rounded,
|
||||
size="10px",
|
||||
:color="barColor(p)",
|
||||
track-color="grey-3"
|
||||
)
|
||||
.t-muted.collective__progress-hint(v-if="p.collecting && reachedMin(p)")
|
||||
//- ЕДИНАЯ карточка партии (канон-виджет SupplyPartyCard) — та же, что у
|
||||
//- поставщика. Отличаются только данные и тексты, не вёрстка.
|
||||
SupplyPartyCard(
|
||||
v-for="p in parties",
|
||||
:key="p.key",
|
||||
:product-name="p.productName",
|
||||
:pvz-name="p.pvzName",
|
||||
:stage-status="p.stageStatus",
|
||||
:order-count="p.orders.length",
|
||||
:volume-label="`Накоплено всеми: ${accumulated(p)} ${p.unitLabel}`",
|
||||
:target-label="hasTarget(p) ? `цель — от ${p.groupMinVolume} ${p.unitLabel}` : ''",
|
||||
:progress="progressRatio(p)",
|
||||
:bar-color="barColor(p)",
|
||||
:members="memberRows(p)",
|
||||
total-label="Ваш вклад в партию",
|
||||
:total-value="`${formatCost(p.ownCost)} · ${p.ownUnits} ${p.unitLabel}`"
|
||||
)
|
||||
template(#hint)
|
||||
template(v-if="p.collecting && reachedMin(p)")
|
||||
q-icon(name="check_circle", size="14px", color="positive")
|
||||
| Минимальный объём набран — поставщик может принять партию.
|
||||
.t-muted.collective__progress-hint(v-else-if="p.collecting")
|
||||
| Сбор продолжается. Поставщик может принять партию и раньше — минимум лишь ориентир.
|
||||
.t-muted.collective__progress-hint(v-else)
|
||||
span Минимальный объём набран — поставщик может принять партию.
|
||||
template(v-else-if="p.collecting")
|
||||
span Сбор продолжается. Поставщик может принять партию и раньше — минимум лишь ориентир.
|
||||
template(v-else)
|
||||
q-icon(name="local_shipping", size="14px")
|
||||
| Партия набрана. Этап: {{ orderStatusDisplay(p.stageStatus).label }}.
|
||||
|
||||
//- Свои заказы в партии — компактными строками (товар/КУ уже в шапке).
|
||||
.collective__own-orders
|
||||
.collective__own-order(v-for="o in p.orders", :key="o.id")
|
||||
span.collective__own-order-who Ваш заказ № {{ o.id.slice(0, 8) }}
|
||||
span.collective__own-order-qty {{ o.quantity }} {{ p.unitLabel }}
|
||||
span.collective__own-order-cost {{ formatCost(parseFloat(o.total_cost) || 0) }}
|
||||
|
||||
.collective__own
|
||||
span.t-muted Ваш вклад в партию
|
||||
span.collective__own-val {{ formatCost(p.ownCost) }} · {{ p.ownUnits }} {{ p.unitLabel }}
|
||||
span Партия набрана. Этап: {{ orderStatusDisplay(p.stageStatus).label }}.
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -279,96 +272,6 @@ q-page.collective(role="region", aria-label="Коллективный заказ
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
}
|
||||
|
||||
&__party {
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
background: var(--p-surface);
|
||||
padding: var(--p-4, 16px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
&__party-sub {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&__progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__progress-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
font-size: var(--p-fs-body);
|
||||
}
|
||||
|
||||
&__progress-bar {
|
||||
border-radius: var(--p-r-sm, 8px);
|
||||
}
|
||||
|
||||
&__progress-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__own-orders {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-sm, 8px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__own-order {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: var(--p-4, 16px);
|
||||
align-items: center;
|
||||
padding: var(--p-2, 8px) var(--p-3, 12px);
|
||||
border-top: 1px solid var(--p-line);
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__own-order-who {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__own-order-qty {
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
&__own-order-cost {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__own {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-2, 8px);
|
||||
padding-top: var(--p-2, 8px);
|
||||
border-top: 1px solid var(--p-line);
|
||||
}
|
||||
|
||||
&__own-val {
|
||||
font-size: var(--p-fs-body);
|
||||
color: var(--p-ink-1);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
+10
-6
@@ -7,6 +7,7 @@ import { BaseBadge, BaseButton, BaseCard } from 'src/shared/ui/base';
|
||||
import { ActivityTimeline, type ActivityEvent } from 'src/shared/ui/domain';
|
||||
import { OfferGallery } from 'src/widgets/Marketplace/OfferGallery';
|
||||
import { RefreshButton } from 'src/widgets/Marketplace/RefreshButton';
|
||||
import { ReceiveCodeDialog } from 'src/widgets/Marketplace/ReceiveCode';
|
||||
import { orderStatusDisplay } from 'src/widgets/Marketplace/OrderCard';
|
||||
import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
|
||||
import { marketplaceOfferImageUrls } from 'src/shared/lib/utils';
|
||||
@@ -37,6 +38,7 @@ const offerImages = ref<string[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const finalizeDialogOpen = ref(false);
|
||||
const receiveDialogOpen = ref(false);
|
||||
|
||||
const status = computed(() => (order.value ? orderStatusDisplay(order.value.status) : null));
|
||||
const unitShort = computed(() => marketplaceUnitShort(order.value?.unit_of_measure));
|
||||
@@ -108,7 +110,7 @@ function goBack(): void {
|
||||
}
|
||||
|
||||
function goReceive(): void {
|
||||
void router.push({ name: 'marketplace-receive-code', params: { coopname: coopname.value } });
|
||||
receiveDialogOpen.value = true;
|
||||
}
|
||||
|
||||
function confirmCancel(): void {
|
||||
@@ -155,6 +157,10 @@ onBeforeUnmount(() => {
|
||||
<template lang="pug">
|
||||
q-page.order-detail(role="region", aria-label="Заказ")
|
||||
Teleport(to="#header-actions-host", defer)
|
||||
BaseButton(variant="secondary", size="sm", @click="goReceive")
|
||||
template(#icon-left)
|
||||
q-icon(name="qr_code_2", size="16px")
|
||||
| Получить заказ
|
||||
RefreshButton(:loading="loading", @refresh="load")
|
||||
|
||||
.order-detail__col
|
||||
@@ -222,11 +228,7 @@ q-page.order-detail(role="region", aria-label="Заказ")
|
||||
.t-h3 Хронология
|
||||
ActivityTimeline(:events="timelineEvents", group-by-date)
|
||||
|
||||
.order-detail__actions
|
||||
BaseButton(variant="secondary", @click="goReceive")
|
||||
template(#icon-left)
|
||||
q-icon(name="qr_code_2", size="16px")
|
||||
| Получить заказ
|
||||
.order-detail__actions(v-if="receivable || cancellable")
|
||||
BaseButton(v-if="receivable", variant="primary", @click="finalizeDialogOpen = true")
|
||||
template(#icon-left)
|
||||
q-icon(name="draw", size="16px")
|
||||
@@ -238,6 +240,8 @@ q-page.order-detail(role="region", aria-label="Заказ")
|
||||
:order="order",
|
||||
@finalized="onFinalized"
|
||||
)
|
||||
|
||||
ReceiveCodeDialog(v-model="receiveDialogOpen", :coopname="coopname")
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
+4
-38
@@ -1,10 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
import { HandoffQr } from 'src/widgets/Marketplace/HandoffQr';
|
||||
import { EmptyState } from 'src/shared/ui/base';
|
||||
import { encodeHandoffToken, HandoffTokenKind } from 'src/shared/lib/marketplace';
|
||||
import { ReceiveCodeContent } from 'src/widgets/Marketplace/ReceiveCode';
|
||||
|
||||
/**
|
||||
* Стол заказчика, страница «Получить заказ».
|
||||
@@ -15,39 +12,17 @@ import { encodeHandoffToken, HandoffTokenKind } from 'src/shared/lib/marketplace
|
||||
*
|
||||
* Сделано отдельным пунктом меню (а не действием в шапке), чтобы код был
|
||||
* очевидно findable: пайщику не нужно объяснять, где его искать — пункт
|
||||
* «Получить заказ» всегда виден в меню стола.
|
||||
* «Получить заказ» всегда виден в меню стола. Тот же QR доступен диалогом из
|
||||
* шапки «Моих заказов» и детали заказа (общий `ReceiveCodeContent`).
|
||||
*/
|
||||
|
||||
const route = useRoute();
|
||||
const session = useSessionStore();
|
||||
const coopname = computed(() => String(route.params.coopname ?? ''));
|
||||
|
||||
const receiveCode = computed(() =>
|
||||
session.username
|
||||
? encodeHandoffToken({
|
||||
kind: HandoffTokenKind.Receive,
|
||||
coopname: coopname.value,
|
||||
account: session.username,
|
||||
})
|
||||
: '',
|
||||
);
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
q-page.receive(role="region", aria-label="Получить заказ")
|
||||
.receive__inner(v-if="receiveCode")
|
||||
HandoffQr(
|
||||
:value="receiveCode",
|
||||
:size="320",
|
||||
caption="Покажите этот код оператору на том пункте выдачи, куда пришёл ваш заказ — он выдаст по нему всё, что готово к получению. Код можно показать с экрана телефона или с распечатки."
|
||||
)
|
||||
EmptyState(
|
||||
v-else,
|
||||
title="Код недоступен",
|
||||
body="Войдите в кооператив, чтобы получить персональный код выдачи."
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name="qr_code_2", size="48px")
|
||||
ReceiveCodeContent(:coopname="coopname")
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -56,14 +31,5 @@ q-page.receive(role="region", aria-label="Получить заказ")
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--p-6, 24px);
|
||||
|
||||
&__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--p-4, 16px);
|
||||
max-width: 480px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -169,35 +169,10 @@ function onClick() {
|
||||
}
|
||||
}
|
||||
|
||||
&__carousel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--mp-surface-1);
|
||||
|
||||
// Слайд q-carousel по умолчанию имеет внутренний padding — обнуляем,
|
||||
// чтобы изображение шло во всю ширину карточки.
|
||||
:deep(.q-carousel__slide) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.q-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.mp-card--interactive:hover &__media :deep(img) {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__status {
|
||||
position: absolute;
|
||||
top: var(--mp-space-sm);
|
||||
|
||||
@@ -50,6 +50,10 @@ withDefaults(
|
||||
/** Высота карусели: фикс ('360px') или '100%' под размер обёртки. */
|
||||
height?: string
|
||||
arrows?: boolean
|
||||
/**
|
||||
* Точки-навигация снизу. По умолчанию выключены — точки в галерее товара
|
||||
* не нужны (листается свайпом/стрелками), их явно убрали по всем экранам.
|
||||
*/
|
||||
navigation?: boolean
|
||||
alt?: string
|
||||
fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
|
||||
@@ -58,7 +62,7 @@ withDefaults(
|
||||
{
|
||||
height: '100%',
|
||||
arrows: true,
|
||||
navigation: true,
|
||||
navigation: false,
|
||||
alt: '',
|
||||
fit: 'cover',
|
||||
placeholderIconSize: '48px',
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
import { HandoffQr } from 'src/widgets/Marketplace/HandoffQr';
|
||||
import { EmptyState } from 'src/shared/ui/base';
|
||||
import { encodeHandoffToken, HandoffTokenKind } from 'src/shared/lib/marketplace';
|
||||
|
||||
/**
|
||||
* Содержимое экрана «Получить заказ» — account-bound QR-код заказчика плюс
|
||||
* пояснение. Единый источник на три точки показа: отдельная страница меню
|
||||
* (`OrdererReceiveCode`), диалог из шапки «Моих заказов» и диалог из шапки
|
||||
* детали заказа. Раньше QR верстали отдельно в каждой — нарушение DRY; теперь
|
||||
* один компонент, которому передают `coopname` (аккаунт берётся из сессии).
|
||||
*/
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
coopname: string;
|
||||
size?: number;
|
||||
}>(),
|
||||
{ size: 320 },
|
||||
);
|
||||
|
||||
const session = useSessionStore();
|
||||
|
||||
const receiveCode = computed(() =>
|
||||
session.username
|
||||
? encodeHandoffToken({
|
||||
kind: HandoffTokenKind.Receive,
|
||||
coopname: props.coopname,
|
||||
account: session.username,
|
||||
})
|
||||
: '',
|
||||
);
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.receive-code(v-if="receiveCode")
|
||||
HandoffQr(
|
||||
:value="receiveCode",
|
||||
:size="size",
|
||||
caption="Покажите этот код оператору на том пункте выдачи, куда пришёл ваш заказ — он выдаст по нему всё, что готово к получению. Код можно показать с экрана телефона или с распечатки."
|
||||
)
|
||||
EmptyState(
|
||||
v-else,
|
||||
title="Код недоступен",
|
||||
body="Войдите в кооператив, чтобы получить персональный код выдачи."
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name="qr_code_2", size="48px")
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.receive-code {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--p-4, 16px);
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
import { BaseDialog } from 'src/shared/ui/base';
|
||||
import ReceiveCodeContent from './ReceiveCodeContent.vue';
|
||||
|
||||
/**
|
||||
* Диалог «Получить заказ» — тот же QR-код, что и на отдельной странице, но
|
||||
* всплывающим окном. Открывается кнопкой из шапки «Моих заказов» и из шапки
|
||||
* детали заказа: код всегда под рукой в одном месте (шапка), без перехода на
|
||||
* отдельную страницу и проблемы «как вернуться назад».
|
||||
*/
|
||||
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
coopname: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
BaseDialog(
|
||||
:model-value="modelValue",
|
||||
title="Получить заказ",
|
||||
@update:model-value="(v: boolean) => emit('update:modelValue', v)"
|
||||
)
|
||||
.receive-code-dialog
|
||||
ReceiveCodeContent(:coopname="coopname")
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.receive-code-dialog {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--p-4, 16px) 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as ReceiveCodeContent } from './ReceiveCodeContent.vue';
|
||||
export { default as ReceiveCodeDialog } from './ReceiveCodeDialog.vue';
|
||||
@@ -0,0 +1,184 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { BaseBadge } from 'src/shared/ui/base';
|
||||
import { orderStatusDisplay, type DomainOrderStatus } from 'src/widgets/Marketplace/OrderCard';
|
||||
|
||||
/**
|
||||
* Единая карточка партии Стола заказов — одна вёрстка на всех столах и стадиях.
|
||||
*
|
||||
* Партия (заказы по паре оферта × КУ) показывается одинаково и у заказчика
|
||||
* («Коллективный заказ»), и у поставщика («Входящие заказы»), на любом этапе
|
||||
* (копится / принята / в работе): шапка (товар + КУ + бейдж этапа + счётчик),
|
||||
* прогресс-бар сбора, состав партии строками, подвал (итог + действия).
|
||||
* Меняются только данные и тексты — не вёрстка. Раньше каждая страница
|
||||
* рисовала свою карточку (накопитель ≠ принятая) — путало и нарушало DRY.
|
||||
*
|
||||
* Вариативные части — слотами:
|
||||
* - `#hint` — пояснение под прогресс-баром (тексты у столов разные);
|
||||
* - `#actions` — кнопки в подвале (у поставщика «Принять/Отклонить», у
|
||||
* заказчика пусто).
|
||||
*/
|
||||
|
||||
const props = defineProps<{
|
||||
productName: string;
|
||||
pvzName: string;
|
||||
/** Доменный статус-этап партии (минимальный по рангу среди заказов). */
|
||||
stageStatus: DomainOrderStatus | string;
|
||||
orderCount: number;
|
||||
/** Левая подпись прогресса целиком, напр. «Объём партии: 5 кг». */
|
||||
volumeLabel: string;
|
||||
/** Правая muted-подпись (цель сбора), напр. «цель — от 10 кг». Пусто — скрыта. */
|
||||
targetLabel?: string;
|
||||
/** Заполнение прогресс-бара 0..1. */
|
||||
progress: number;
|
||||
/** Цвет бара (Quasar color): primary пока копится, positive когда набрано/принято. */
|
||||
barColor: string;
|
||||
/** Состав партии строками: кто · сколько · стоимость. */
|
||||
members: Array<{ id: string; who: string; qty: string; cost: string }>;
|
||||
/** Подпись итога, напр. «Итого партии» / «Ваш вклад в партию». */
|
||||
totalLabel: string;
|
||||
/** Значение итога, напр. «1 200 ₽ · 5 кг». */
|
||||
totalValue: string;
|
||||
}>();
|
||||
|
||||
const statusDisplay = computed(() => orderStatusDisplay(props.stageStatus));
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
.supply-party
|
||||
.supply-party__head
|
||||
.row.items-center.q-gutter-sm.no-wrap
|
||||
div.col
|
||||
.t-h3 {{ productName }}
|
||||
.t-muted.supply-party__sub
|
||||
q-icon(name="place", size="14px")
|
||||
| КУ «{{ pvzName }}»
|
||||
BaseBadge(:variant="statusDisplay.variant") {{ statusDisplay.label }}
|
||||
span.chip.chip--accent
|
||||
q-icon(name="layers", size="14px")
|
||||
| {{ orderCount }} зак.
|
||||
|
||||
.supply-party__progress
|
||||
.supply-party__progress-row
|
||||
span {{ volumeLabel }}
|
||||
span.t-muted(v-if="targetLabel") {{ targetLabel }}
|
||||
q-linear-progress.supply-party__progress-bar(
|
||||
:value="progress",
|
||||
rounded,
|
||||
size="10px",
|
||||
:color="barColor",
|
||||
track-color="grey-3"
|
||||
)
|
||||
.t-muted.supply-party__progress-hint
|
||||
slot(name="hint")
|
||||
|
||||
.supply-party__members
|
||||
.supply-party__member(v-for="m in members", :key="m.id")
|
||||
span.supply-party__member-who {{ m.who }}
|
||||
span.supply-party__member-qty {{ m.qty }}
|
||||
span.supply-party__member-cost {{ m.cost }}
|
||||
|
||||
.supply-party__foot
|
||||
.supply-party__total
|
||||
span.t-muted {{ totalLabel }}
|
||||
span.supply-party__total-val {{ totalValue }}
|
||||
q-space
|
||||
slot(name="actions")
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.supply-party {
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
background: var(--p-surface);
|
||||
padding: var(--p-4, 16px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3, 12px);
|
||||
|
||||
&__sub {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&__progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__progress-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
font-size: var(--p-fs-body);
|
||||
}
|
||||
|
||||
&__progress-bar {
|
||||
border-radius: var(--p-r-sm, 8px);
|
||||
}
|
||||
|
||||
&__progress-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__members {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-sm, 8px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__member {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: var(--p-4, 16px);
|
||||
align-items: center;
|
||||
padding: var(--p-2, 8px) var(--p-3, 12px);
|
||||
border-top: 1px solid var(--p-line);
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__member-who {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__member-qty {
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
&__member-cost {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-2, 8px);
|
||||
flex-wrap: wrap;
|
||||
padding-top: var(--p-2, 8px);
|
||||
border-top: 1px solid var(--p-line);
|
||||
}
|
||||
|
||||
&__total {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__total-val {
|
||||
font-size: var(--p-fs-body);
|
||||
color: var(--p-ink-1);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SupplyPartyCard } from './SupplyPartyCard.vue';
|
||||
Reference in New Issue
Block a user