- Add cabinet layout with 1/6 sidebar - Header: rename "Мои заказы" to "Личный кабинет" - Add cabinet pages: orders, offers (seller only), new offer - TeamCreateForm: add team type selection (BUYER/SELLER) - Sidebar shows "Мои предложения" only for SELLER teams 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
151 lines
4.6 KiB
Vue
151 lines
4.6 KiB
Vue
<template>
|
||
<Stack gap="6">
|
||
<Stack direction="row" justify="between" align="center">
|
||
<Heading :level="1">Мои предложения</Heading>
|
||
<NuxtLink :to="localePath('/dashboard/cabinet/offers/new')">
|
||
<Button>
|
||
<Icon name="lucide:plus" size="16" class="mr-2" />
|
||
Добавить
|
||
</Button>
|
||
</NuxtLink>
|
||
</Stack>
|
||
|
||
<Alert v-if="hasError" variant="error">
|
||
<Stack gap="2">
|
||
<Heading :level="4" weight="semibold">Ошибка</Heading>
|
||
<Text tone="muted">{{ error }}</Text>
|
||
<Button @click="loadOffers">Попробовать снова</Button>
|
||
</Stack>
|
||
</Alert>
|
||
|
||
<Stack v-else-if="isLoading" align="center" justify="center" gap="3">
|
||
<Spinner />
|
||
<Text tone="muted">Загружаем предложения...</Text>
|
||
</Stack>
|
||
|
||
<template v-else>
|
||
<Stack v-if="offers.length" gap="4">
|
||
<Card v-for="offer in offers" :key="offer.uuid" padding="lg">
|
||
<Stack gap="3">
|
||
<Stack direction="row" justify="between" align="center">
|
||
<Heading :level="3">{{ offer.title || 'Без названия' }}</Heading>
|
||
<Badge :variant="getStatusVariant(offer.status)">
|
||
{{ getStatusText(offer.status) }}
|
||
</Badge>
|
||
</Stack>
|
||
|
||
<Text v-if="offer.description" tone="muted">{{ offer.description }}</Text>
|
||
|
||
<Grid :cols="1" :md="3" :gap="3">
|
||
<Stack gap="1">
|
||
<Text size="base" weight="semibold">Локация</Text>
|
||
<Text tone="muted">{{ offer.locationName || 'Не указана' }}</Text>
|
||
</Stack>
|
||
|
||
<Stack gap="1">
|
||
<Text size="base" weight="semibold">Товары</Text>
|
||
<Text tone="muted">
|
||
{{ offer.lines?.[0]?.productName || 'Нет товаров' }}
|
||
<template v-if="offer.lines?.length > 1">
|
||
+{{ offer.lines.length - 1 }} ещё
|
||
</template>
|
||
</Text>
|
||
</Stack>
|
||
|
||
<Stack gap="1">
|
||
<Text size="base" weight="semibold">Действует до</Text>
|
||
<Text tone="muted">{{ formatDate(offer.validUntil) }}</Text>
|
||
</Stack>
|
||
</Grid>
|
||
</Stack>
|
||
</Card>
|
||
</Stack>
|
||
|
||
<Stack v-else align="center" gap="3">
|
||
<IconCircle tone="primary">
|
||
<Icon name="lucide:package" size="24" />
|
||
</IconCircle>
|
||
<Heading :level="3">Нет предложений</Heading>
|
||
<Text tone="muted">Создайте своё первое предложение</Text>
|
||
<NuxtLink :to="localePath('/dashboard/cabinet/offers/new')">
|
||
<Button>
|
||
<Icon name="lucide:plus" size="16" class="mr-2" />
|
||
Добавить предложение
|
||
</Button>
|
||
</NuxtLink>
|
||
</Stack>
|
||
</template>
|
||
</Stack>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { GetOffersDocument } from '~/composables/graphql/team/exchange-generated'
|
||
|
||
definePageMeta({
|
||
layout: 'cabinet',
|
||
middleware: ['auth-oidc']
|
||
})
|
||
|
||
const localePath = useLocalePath()
|
||
const { execute } = useGraphQL()
|
||
|
||
const offers = ref<any[]>([])
|
||
const isLoading = ref(true)
|
||
const hasError = ref(false)
|
||
const error = ref('')
|
||
|
||
const loadOffers = async () => {
|
||
try {
|
||
isLoading.value = true
|
||
hasError.value = false
|
||
const result = await execute(GetOffersDocument, {}, 'team', 'exchange')
|
||
offers.value = result.getOffers || []
|
||
} catch (err: any) {
|
||
hasError.value = true
|
||
error.value = err.message || 'Не удалось загрузить предложения'
|
||
offers.value = []
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
const getStatusVariant = (status: string) => {
|
||
const variants: Record<string, string> = {
|
||
active: 'success',
|
||
draft: 'warning',
|
||
expired: 'error',
|
||
sold: 'muted'
|
||
}
|
||
return variants[status] || 'muted'
|
||
}
|
||
|
||
const getStatusText = (status: string) => {
|
||
const texts: Record<string, string> = {
|
||
active: 'Активно',
|
||
draft: 'Черновик',
|
||
expired: 'Истекло',
|
||
sold: 'Продано'
|
||
}
|
||
return texts[status] || status || 'Неизвестно'
|
||
}
|
||
|
||
const formatDate = (date: string) => {
|
||
if (!date) return 'Не указано'
|
||
try {
|
||
const dateObj = new Date(date)
|
||
if (isNaN(dateObj.getTime())) return 'Невалидная дата'
|
||
return new Intl.DateTimeFormat('ru-RU', {
|
||
day: 'numeric',
|
||
month: 'long',
|
||
year: 'numeric'
|
||
}).format(dateObj)
|
||
} catch {
|
||
return 'Невалидная дата'
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadOffers()
|
||
})
|
||
</script>
|