feat(marketplace): admin-стол «Пункты выдачи заказов» — подключение КУ как ПВЗ
Новая страница market-admin: соединяет список КУ (core getBranches) с их ПВЗ-детализациями (marketplaceListKUDetails), председатель подключает участок как пункт выдачи (адрес/контакты/режим работы → marketplaceDetailKU) и управляет статусом/геокодингом. Диалог детализации переведён на канон (BaseDialog/BaseButton + raw q-form с :rules), предзаполнение из карточки КУ, правила времени/email вынесены в script (pug съедает backslash в inline-regex). Статус-бейдж без dot (dot скрывал подпись), тултип с ошибкой геокода. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import { OffererSupplyPreparationPage } from 'src/pages/Marketplace/OffererSuppl
|
||||
import { OffererPaymentHistoryPage } from 'src/pages/Marketplace/OffererPaymentHistory'
|
||||
import { AdminWriteoffsPage } from 'src/pages/Marketplace/AdminWriteoffs'
|
||||
import { ChairmanModerationPage } from 'src/pages/Marketplace/ChairmanModeration'
|
||||
import { AdminIssuancePointsPage } from 'src/pages/Marketplace/AdminIssuancePoints'
|
||||
import { OperatorOwnWarehousePage } from 'src/pages/Marketplace/OperatorOwnWarehouse'
|
||||
import { OperatorIncomingShipmentsPage } from 'src/pages/Marketplace/OperatorIncomingShipments'
|
||||
import { AdminWarehouseSummaryPage } from 'src/pages/Marketplace/AdminWarehouseSummary'
|
||||
@@ -508,6 +509,25 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
// Эпик 2: admin-стол «Пункты выдачи заказов». Председатель
|
||||
// подключает кооперативные участки (core) как ПВЗ Стола заказов
|
||||
// (адрес/контакты/режим работы + геокодинг) и управляет их
|
||||
// статусом. Видна совету и председателю (`Order:read:all`);
|
||||
// управляющие действия внутри — только председателю (isChairman),
|
||||
// что совпадает с бэкенд-авторизацией мутаций (chairman-only).
|
||||
path: 'issuance-points',
|
||||
name: 'marketplace-issuance-points',
|
||||
component: markRaw(AdminIssuancePointsPage),
|
||||
meta: {
|
||||
title: 'Пункты выдачи заказов',
|
||||
icon: 'pin_drop',
|
||||
requires: 'Order:read:all',
|
||||
requiresAuth: true,
|
||||
agreements: agreementsBase,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
// Эпик 8 / Story 8.7: admin-стол списания скоропорта.
|
||||
path: 'writeoffs',
|
||||
|
||||
+202
-73
@@ -1,90 +1,125 @@
|
||||
<template lang="pug">
|
||||
q-dialog(:model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" persistent)
|
||||
q-card(style="min-width: 480px; max-width: 720px;")
|
||||
q-card-section.row.items-center
|
||||
.text-h6 Детализация ПВЗ — {{ coreBraname }}
|
||||
q-space
|
||||
q-btn(flat round icon="close" @click="cancel")
|
||||
q-card-section
|
||||
q-form(@submit.prevent="submit" greedy)
|
||||
q-input(
|
||||
v-model="form.addressFull"
|
||||
label="Полный адрес ПВЗ"
|
||||
required
|
||||
autogrow
|
||||
:rules="[v => !!v || 'Адрес обязателен']"
|
||||
aria-label="Полный адрес пункта выдачи"
|
||||
)
|
||||
BaseDialog(
|
||||
:model-value="modelValue"
|
||||
:title="`Детализация ПВЗ — ${coreBraname}`"
|
||||
size="lg"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
)
|
||||
.detail-ku
|
||||
.banner.banner--info
|
||||
q-icon.banner__icon(name="info", size="18px")
|
||||
.banner__body
|
||||
| Укажите фактический адрес, контакты и режим работы пункта выдачи —
|
||||
| он может отличаться от юридического адреса участка. После сохранения
|
||||
| адрес автоматически геокодируется для отображения на карте.
|
||||
|
||||
q-form.detail-ku__form(ref="formRef", greedy)
|
||||
q-input(
|
||||
v-model="form.addressFull"
|
||||
outlined
|
||||
dense
|
||||
label="Фактический адрес ПВЗ"
|
||||
autogrow
|
||||
:rules="[v => !!(v && v.trim()) || 'Адрес обязателен']"
|
||||
aria-label="Фактический адрес пункта выдачи"
|
||||
)
|
||||
.detail-ku__row
|
||||
q-input(
|
||||
v-model="form.contactPhone"
|
||||
outlined
|
||||
dense
|
||||
label="Контактный телефон"
|
||||
mask="+# (###) ###-##-##"
|
||||
required
|
||||
:rules="[v => !!v || 'Телефон обязателен']"
|
||||
aria-label="Контактный телефон пункта выдачи"
|
||||
)
|
||||
q-input(
|
||||
v-model="form.contactEmail"
|
||||
outlined
|
||||
dense
|
||||
label="Контактный email"
|
||||
type="email"
|
||||
required
|
||||
:rules="[v => /.+@.+\..+/.test(v) || 'Введите валидный email']"
|
||||
lazy-rules
|
||||
:rules="[emailRule]"
|
||||
aria-label="Контактный email пункта выдачи"
|
||||
)
|
||||
q-input(
|
||||
v-model="form.description"
|
||||
label="Описание (опционально)"
|
||||
type="textarea"
|
||||
autogrow
|
||||
aria-label="Описание пункта выдачи"
|
||||
)
|
||||
.text-subtitle2.q-mt-md Режим работы
|
||||
.row.q-col-gutter-sm
|
||||
.col-12.col-sm-6(v-for="day in days" :key="day.key")
|
||||
q-card.q-pa-sm(flat bordered)
|
||||
.row.items-center.q-mb-xs
|
||||
q-toggle(
|
||||
v-model="form.workingHours[day.key].enabled"
|
||||
:label="day.label"
|
||||
dense
|
||||
)
|
||||
.row.q-col-gutter-xs(v-if="form.workingHours[day.key].enabled")
|
||||
.col
|
||||
q-input(
|
||||
v-model="form.workingHours[day.key].open"
|
||||
label="Открытие"
|
||||
mask="##:##"
|
||||
placeholder="09:00"
|
||||
dense
|
||||
:rules="[v => /^\d{2}:\d{2}$/.test(v) || 'HH:mm']"
|
||||
)
|
||||
.col
|
||||
q-input(
|
||||
v-model="form.workingHours[day.key].close"
|
||||
label="Закрытие"
|
||||
mask="##:##"
|
||||
placeholder="18:00"
|
||||
dense
|
||||
:rules="[v => /^\d{2}:\d{2}$/.test(v) || 'HH:mm']"
|
||||
)
|
||||
q-card-actions(align="right")
|
||||
q-btn(flat label="Отмена" @click="cancel")
|
||||
q-btn(color="primary" label="Сохранить" :loading="isSaving" @click="submit")
|
||||
q-input(
|
||||
v-model="form.description"
|
||||
outlined
|
||||
dense
|
||||
label="Описание (опционально)"
|
||||
type="textarea"
|
||||
autogrow
|
||||
aria-label="Описание пункта выдачи"
|
||||
)
|
||||
|
||||
.detail-ku__section-title Режим работы
|
||||
.detail-ku__days
|
||||
.detail-ku__day(v-for="day in days", :key="day.key")
|
||||
q-toggle(
|
||||
v-model="form.workingHours[day.key].enabled"
|
||||
:label="day.label"
|
||||
dense
|
||||
)
|
||||
.detail-ku__day-times(v-if="form.workingHours[day.key].enabled")
|
||||
q-input(
|
||||
v-model="form.workingHours[day.key].open"
|
||||
outlined
|
||||
dense
|
||||
label="Открытие"
|
||||
mask="time"
|
||||
placeholder="09:00"
|
||||
lazy-rules
|
||||
:rules="[timeRule]"
|
||||
)
|
||||
q-input(
|
||||
v-model="form.workingHours[day.key].close"
|
||||
outlined
|
||||
dense
|
||||
label="Закрытие"
|
||||
mask="time"
|
||||
placeholder="18:00"
|
||||
lazy-rules
|
||||
:rules="[timeRule]"
|
||||
)
|
||||
|
||||
template(#footer)
|
||||
BaseButton(variant="ghost", :disabled="isSaving", @click="cancel") Отмена
|
||||
BaseButton(variant="primary", :loading="isSaving", @click="submit")
|
||||
template(#icon-left)
|
||||
q-icon(name="save", size="16px")
|
||||
| Сохранить
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import type { QForm } from 'quasar'
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api'
|
||||
import { BaseButton, BaseDialog } from 'src/shared/ui/base'
|
||||
import { useMarketplaceKUDetailsStore } from 'src/entities/MarketplaceKUDetails'
|
||||
import type {
|
||||
IMarketplaceKUDetails,
|
||||
IWorkingHours,
|
||||
} from 'src/entities/MarketplaceKUDetails'
|
||||
import type { IBranch } from 'src/entities/Branch/model'
|
||||
|
||||
/**
|
||||
* Детализация существующего в core кооперативного участка как ПВЗ Стола
|
||||
* заказов (`marketplaceDetailKU`). Используется и для добавления нового ПВЗ
|
||||
* (передаётся `branch` — поля адреса/контактов предзаполняются из карточки
|
||||
* КУ стола совета), и для редактирования уже подключённого (`existing`).
|
||||
*
|
||||
* Поля с масками/валидацией остаются на сыром `q-form`/`q-input` — это
|
||||
* одобренный канон форм проекта (см. CreateMarketplaceOfferPage); base-обёртки
|
||||
* (BaseDialog/BaseButton) применяются к оболочке и действиям.
|
||||
*/
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
coopname: string
|
||||
coreBraname: string
|
||||
existing?: IMarketplaceKUDetails | null
|
||||
branch?: IBranch | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -102,9 +137,24 @@ const days = [
|
||||
{ key: 'sun' as const, label: 'Вс' },
|
||||
]
|
||||
|
||||
type DayKey = (typeof days)[number]['key']
|
||||
|
||||
const isSaving = ref(false)
|
||||
const formRef = ref<QForm | null>(null)
|
||||
const store = useMarketplaceKUDetailsStore()
|
||||
|
||||
// Правила с регулярками держим в script, а не inline в pug-атрибуте:
|
||||
// pug съедает обратные слэши в строковом значении :rules, и `\d`/`\.`
|
||||
// деградируют в литералы `d`/`.`, из-за чего валидные значения ложно
|
||||
// краснеют (канон форм — выносить такие правила в функции, ср. priceRule).
|
||||
function emailRule(v: string): true | string {
|
||||
return /.+@.+\..+/.test(v) || 'Введите валидный email'
|
||||
}
|
||||
|
||||
function timeRule(v: string): true | string {
|
||||
return /^([01]\d|2[0-3]):[0-5]\d$/.test(v) || 'Время в формате ЧЧ:ММ'
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
addressFull: '',
|
||||
contactPhone: '',
|
||||
@@ -112,24 +162,43 @@ const form = reactive({
|
||||
description: '',
|
||||
workingHours: Object.fromEntries(
|
||||
days.map((d) => [d.key, { enabled: false, open: '09:00', close: '18:00' }])
|
||||
) as Record<(typeof days)[number]['key'], { enabled: boolean; open: string; close: string }>,
|
||||
) as Record<DayKey, { enabled: boolean; open: string; close: string }>,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.existing,
|
||||
(next) => {
|
||||
if (next) {
|
||||
form.addressFull = next.addressFull
|
||||
form.contactPhone = next.contactPhone
|
||||
form.contactEmail = next.contactEmail
|
||||
form.description = next.description ?? ''
|
||||
for (const d of days) {
|
||||
const wh = next.workingHours[d.key]
|
||||
form.workingHours[d.key] = wh
|
||||
? { enabled: true, open: wh.open, close: wh.close }
|
||||
: { enabled: false, open: '09:00', close: '18:00' }
|
||||
}
|
||||
function resetWorkingHours() {
|
||||
for (const d of days) {
|
||||
form.workingHours[d.key] = { enabled: false, open: '09:00', close: '18:00' }
|
||||
}
|
||||
}
|
||||
|
||||
// Перезаполнение при открытии: редактирование берёт значения из `existing`,
|
||||
// добавление нового — из карточки КУ (фактический адрес → контакты).
|
||||
function resetForm() {
|
||||
const next = props.existing
|
||||
if (next) {
|
||||
form.addressFull = next.addressFull
|
||||
form.contactPhone = next.contactPhone
|
||||
form.contactEmail = next.contactEmail
|
||||
form.description = next.description ?? ''
|
||||
for (const d of days) {
|
||||
const wh = next.workingHours[d.key]
|
||||
form.workingHours[d.key] = wh
|
||||
? { enabled: true, open: wh.open, close: wh.close }
|
||||
: { enabled: false, open: '09:00', close: '18:00' }
|
||||
}
|
||||
} else {
|
||||
form.addressFull = props.branch?.fact_address || props.branch?.full_address || ''
|
||||
form.contactPhone = props.branch?.phone || ''
|
||||
form.contactEmail = props.branch?.email || ''
|
||||
form.description = ''
|
||||
resetWorkingHours()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (open) resetForm()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
@@ -148,6 +217,9 @@ function cancel() {
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) return
|
||||
|
||||
isSaving.value = true
|
||||
try {
|
||||
const saved = await store.detailKU({
|
||||
@@ -159,10 +231,67 @@ async function submit() {
|
||||
description: form.description || undefined,
|
||||
workingHours: buildWorkingHours(),
|
||||
})
|
||||
SuccessAlert('Пункт выдачи сохранён. Адрес геокодируется автоматически.')
|
||||
emit('saved', saved)
|
||||
emit('update:modelValue', false)
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось сохранить пункт выдачи')
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-ku {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
|
||||
&__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin-top: var(--p-2, 8px);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
&__days {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
&__day {
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md, 8px);
|
||||
padding: var(--p-3, 12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__day-times {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.detail-ku__row,
|
||||
.detail-ku__days {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ui'
|
||||
+368
@@ -0,0 +1,368 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api'
|
||||
import { useDismissibleBanner } from 'src/shared/hooks'
|
||||
import { useSessionStore } from 'src/entities/Session'
|
||||
import { useBranchStore } from 'src/entities/Branch/model'
|
||||
import type { IBranch } from 'src/entities/Branch/model'
|
||||
import { useMarketplaceKUDetailsStore } from 'src/entities/MarketplaceKUDetails'
|
||||
import type {
|
||||
GeocodeStatus,
|
||||
IMarketplaceKUDetails,
|
||||
KuDetailsStatus,
|
||||
} from 'src/entities/MarketplaceKUDetails'
|
||||
import { BaseBadge, BaseButton, EmptyState, TableSkeleton } from 'src/shared/ui/base'
|
||||
import type { BaseBadgeVariant, TableSkeletonColumn } from 'src/shared/ui/base'
|
||||
import { MarketplaceDetailKUDialog } from 'src/features/MarketplaceDetailKU'
|
||||
|
||||
/**
|
||||
* Эпик 2: admin-стол «Пункты выдачи заказов».
|
||||
*
|
||||
* Соединяет список ВСЕХ кооперативных участков кооператива (core `getBranches`,
|
||||
* создаются на столе совета) с их marketplace-детализациями ПВЗ
|
||||
* (`marketplaceListKUDetails`). Председатель видит, какие КУ уже подключены как
|
||||
* пункты выдачи, и подключает новые: «Сделать ПВЗ» открывает диалог с
|
||||
* предзаполнением адреса/контактов из карточки КУ → `marketplaceDetailKU`.
|
||||
*
|
||||
* Управляющие действия (подключить/изменить/геокодинг/активация) — только
|
||||
* председателю; совет видит сеть ПВЗ в режиме чтения.
|
||||
*/
|
||||
|
||||
const route = useRoute()
|
||||
const session = useSessionStore()
|
||||
const branchStore = useBranchStore()
|
||||
const kuStore = useMarketplaceKUDetailsStore()
|
||||
|
||||
const coopname = computed(() => String(route.params.coopname ?? ''))
|
||||
const isChairman = computed(() => session.isChairman ?? false)
|
||||
|
||||
const loading = ref(false)
|
||||
const { dismissed: bannerDismissed, dismiss: dismissBanner } = useDismissibleBanner(
|
||||
'mp:admin-pvz:banner-dismissed',
|
||||
)
|
||||
|
||||
const dialogOpen = ref(false)
|
||||
const dialogBranch = ref<IBranch | null>(null)
|
||||
const dialogExisting = ref<IMarketplaceKUDetails | null>(null)
|
||||
|
||||
interface IssuancePointRow {
|
||||
branch: IBranch
|
||||
details: IMarketplaceKUDetails | null
|
||||
}
|
||||
|
||||
// КУ + его ПВЗ-детализация (если подключён). КУ без детализации — кандидат
|
||||
// на подключение; подключённые показываются со статусом и геокодом.
|
||||
const rows = computed<IssuancePointRow[]>(() => {
|
||||
const byBraname = new Map(kuStore.details.map((d) => [d.coreBraname, d]))
|
||||
return branchStore.branches.map((branch) => ({
|
||||
branch,
|
||||
details: byBraname.get(branch.braname) ?? null,
|
||||
}))
|
||||
})
|
||||
|
||||
const connectedCount = computed(() => rows.value.filter((r) => r.details).length)
|
||||
|
||||
const STATUS_LABEL: Record<KuDetailsStatus, { label: string; variant: BaseBadgeVariant }> = {
|
||||
ACTIVE: { label: 'Активен', variant: 'pos' },
|
||||
INACTIVE: { label: 'Деактивирован', variant: 'neutral' },
|
||||
}
|
||||
|
||||
const GEOCODE_LABEL: Record<GeocodeStatus, { label: string; variant: BaseBadgeVariant }> = {
|
||||
OK: { label: 'Координаты есть', variant: 'pos' },
|
||||
PENDING: { label: 'Определяются…', variant: 'warn' },
|
||||
FAILED: { label: 'Ошибка геокода', variant: 'neg' },
|
||||
}
|
||||
|
||||
function statusOf(row: IssuancePointRow): { label: string; variant: BaseBadgeVariant } {
|
||||
if (!row.details) return { label: 'Не подключён', variant: 'neutral' }
|
||||
return STATUS_LABEL[row.details.status]
|
||||
}
|
||||
|
||||
function addressOf(row: IssuancePointRow): string {
|
||||
if (row.details) return row.details.addressFull
|
||||
return row.branch.fact_address || row.branch.full_address || '—'
|
||||
}
|
||||
|
||||
const skeletonColumns: TableSkeletonColumn[] = [
|
||||
{ label: 'Участок', cell: 'text' },
|
||||
{ label: 'Город', cell: 'text', cellWidth: '120px' },
|
||||
{ label: 'Адрес', cell: 'text' },
|
||||
{ label: 'Статус', cell: 'badge' },
|
||||
{ label: 'Геокод', cell: 'badge' },
|
||||
{ label: 'Действия', class: 'col-action', cell: 'icon' },
|
||||
]
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
branchStore.loadBranches({ coopname: coopname.value }),
|
||||
kuStore.load({ coopname: coopname.value, onlyActive: false }),
|
||||
])
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить участки и пункты выдачи')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openAdd(branch: IBranch): void {
|
||||
dialogBranch.value = branch
|
||||
dialogExisting.value = null
|
||||
dialogOpen.value = true
|
||||
}
|
||||
|
||||
function openEdit(row: IssuancePointRow): void {
|
||||
dialogBranch.value = row.branch
|
||||
dialogExisting.value = row.details
|
||||
dialogOpen.value = true
|
||||
}
|
||||
|
||||
async function onSaved(): Promise<void> {
|
||||
await load()
|
||||
}
|
||||
|
||||
async function setStatus(row: IssuancePointRow, status: KuDetailsStatus): Promise<void> {
|
||||
try {
|
||||
await kuStore.setStatus({ coopname: coopname.value, coreBraname: row.branch.braname, status })
|
||||
SuccessAlert(status === 'ACTIVE' ? 'Пункт выдачи активирован' : 'Пункт выдачи деактивирован')
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось изменить статус пункта выдачи')
|
||||
}
|
||||
}
|
||||
|
||||
async function retryGeocode(row: IssuancePointRow): Promise<void> {
|
||||
try {
|
||||
await kuStore.retryGeocode(coopname.value, row.branch.braname)
|
||||
SuccessAlert('Геокодинг адреса перезапущен')
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось перезапустить геокодинг')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
q-page.admin-pvz
|
||||
.banner.banner--info(v-if='!bannerDismissed')
|
||||
q-icon.banner__icon(name='info', size='18px')
|
||||
.banner__body
|
||||
| Пункты выдачи заказов — это кооперативные участки, подключённые к Столу
|
||||
| заказов. Участки создаются на столе совета; здесь председатель делает их
|
||||
| пунктами выдачи, указывая фактический адрес, контакты и режим работы.
|
||||
| Адрес геокодируется автоматически для карты.
|
||||
BaseButton.admin-pvz__banner-close(
|
||||
variant='ghost',
|
||||
icon-only,
|
||||
size='sm',
|
||||
aria-label='Скрыть подсказку',
|
||||
@click='dismissBanner'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='close', size='16px')
|
||||
|
||||
.admin-pvz__toolbar
|
||||
.admin-pvz__counter(v-if='!loading && rows.length')
|
||||
| Подключено пунктов выдачи: {{ connectedCount }} из {{ rows.length }}
|
||||
q-space
|
||||
BaseButton(
|
||||
variant='ghost',
|
||||
icon-only,
|
||||
aria-label='Обновить',
|
||||
:loading='loading',
|
||||
@click='load'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='refresh', size='20px')
|
||||
|
||||
TableSkeleton(
|
||||
v-if='loading && !rows.length',
|
||||
:columns='skeletonColumns',
|
||||
:rows='5',
|
||||
min-width='980px'
|
||||
)
|
||||
.table-wrap(v-else-if='rows.length')
|
||||
.table-scroll
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th.col-ku Участок
|
||||
th.col-city Город
|
||||
th Адрес
|
||||
th.col-status Статус
|
||||
th.col-geo Геокод
|
||||
th.col-action Действия
|
||||
tbody
|
||||
tr(v-for='row in rows', :key='row.branch.braname')
|
||||
td.col-ku
|
||||
.admin-pvz__ku-name {{ row.branch.short_name || row.branch.full_name || row.branch.braname }}
|
||||
.admin-pvz__ku-acc {{ row.branch.braname }}
|
||||
td.col-city {{ row.branch.city || '—' }}
|
||||
td.admin-pvz__address {{ addressOf(row) }}
|
||||
td.col-status
|
||||
BaseBadge(:variant='statusOf(row).variant') {{ statusOf(row).label }}
|
||||
td.col-geo
|
||||
BaseBadge(
|
||||
v-if='row.details',
|
||||
:variant='GEOCODE_LABEL[row.details.geocodeStatus].variant'
|
||||
)
|
||||
| {{ GEOCODE_LABEL[row.details.geocodeStatus].label }}
|
||||
q-tooltip(
|
||||
v-if='row.details.geocodeStatus === "FAILED" && row.details.geocodeErrorMessage'
|
||||
) {{ row.details.geocodeErrorMessage }}
|
||||
span.admin-pvz__dash(v-else) —
|
||||
td.col-action
|
||||
template(v-if='isChairman')
|
||||
BaseButton(
|
||||
v-if='!row.details',
|
||||
variant='primary',
|
||||
size='sm',
|
||||
@click='openAdd(row.branch)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='add_location_alt', size='16px')
|
||||
| Сделать ПВЗ
|
||||
.admin-pvz__actions(v-else)
|
||||
BaseButton(
|
||||
variant='ghost',
|
||||
icon-only,
|
||||
size='sm',
|
||||
aria-label='Изменить',
|
||||
@click='openEdit(row)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='edit', size='18px')
|
||||
BaseButton(
|
||||
v-if='row.details.geocodeStatus !== "OK"',
|
||||
variant='ghost',
|
||||
icon-only,
|
||||
size='sm',
|
||||
aria-label='Перезапустить геокодинг',
|
||||
@click='retryGeocode(row)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='my_location', size='18px')
|
||||
BaseButton(
|
||||
v-if='row.details.status === "ACTIVE"',
|
||||
variant='ghost',
|
||||
icon-only,
|
||||
size='sm',
|
||||
aria-label='Деактивировать',
|
||||
@click='setStatus(row, "INACTIVE")'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='block', size='18px')
|
||||
BaseButton(
|
||||
v-else,
|
||||
variant='ghost',
|
||||
icon-only,
|
||||
size='sm',
|
||||
aria-label='Активировать',
|
||||
@click='setStatus(row, "ACTIVE")'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='check_circle', size='18px')
|
||||
span.admin-pvz__dash(v-else) —
|
||||
|
||||
EmptyState(
|
||||
v-else,
|
||||
title='Кооперативных участков нет',
|
||||
body='Создайте кооперативный участок на столе совета — после этого его можно будет подключить как пункт выдачи заказов.'
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name='pin_drop', size='48px')
|
||||
|
||||
MarketplaceDetailKUDialog(
|
||||
v-if='dialogBranch',
|
||||
v-model='dialogOpen',
|
||||
:coopname='coopname',
|
||||
:core-braname='dialogBranch.braname',
|
||||
:existing='dialogExisting',
|
||||
:branch='dialogBranch',
|
||||
@saved='onSaved'
|
||||
)
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-pvz {
|
||||
padding: var(--p-6, 24px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
|
||||
&__banner-close {
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
margin: -4px -4px 0 0;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
&__counter {
|
||||
color: var(--p-ink-3);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
&__ku-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__ku-acc {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--p-ink-3);
|
||||
}
|
||||
|
||||
&__address {
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--p-1, 4px);
|
||||
}
|
||||
|
||||
&__dash {
|
||||
color: var(--p-ink-3);
|
||||
}
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.table {
|
||||
table-layout: fixed;
|
||||
min-width: 980px;
|
||||
}
|
||||
.col-ku {
|
||||
width: 240px;
|
||||
}
|
||||
.col-city {
|
||||
width: 140px;
|
||||
}
|
||||
.col-status {
|
||||
width: 160px;
|
||||
}
|
||||
.col-geo {
|
||||
width: 170px;
|
||||
}
|
||||
.col-action {
|
||||
width: 200px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-pvz {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as AdminIssuancePointsPage } from './AdminIssuancePointsPage.vue';
|
||||
Reference in New Issue
Block a user