Files
webapp/app/pages/clientarea/offers/[uuid].vue
Ruslan Bakiev ee7b8d0ee4
Some checks failed
Build Docker Image / build (push) Has been cancelled
Remove default layout, migrate all pages to topnav
- Add layout: 'topnav' to all 27 pages that were using default layout
- Delete app/layouts/default.vue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 09:44:26 +07:00

283 lines
9.5 KiB
Vue

<template>
<Stack gap="6">
<!-- Header -->
<Stack direction="row" align="center" justify="between">
<Heading :level="1">{{ t('clientOfferForm.header.title') }}</Heading>
<NuxtLink :to="localePath('/clientarea/offers/new')">
<Button variant="outline">
<Icon name="lucide:arrow-left" size="16" class="mr-2" />
{{ t('clientOfferForm.actions.back') }}
</Button>
</NuxtLink>
</Stack>
<!-- Error -->
<Alert v-if="hasError" variant="error">
<Stack gap="2">
<Heading :level="4" weight="semibold">{{ t('clientOfferForm.error.title') }}</Heading>
<Text tone="muted">{{ error }}</Text>
<Button @click="loadData">{{ t('clientOfferForm.error.retry') }}</Button>
</Stack>
</Alert>
<!-- Loading -->
<Card v-else-if="isLoading" tone="muted" padding="lg">
<Stack align="center" justify="center" gap="3">
<Spinner />
<Text tone="muted">{{ t('clientOfferForm.states.loading') }}</Text>
</Stack>
</Card>
<!-- No schema -->
<Card v-else-if="!schemaId" padding="lg">
<Stack align="center" gap="4">
<IconCircle tone="warning" size="lg">
<Icon name="lucide:alert-triangle" size="24" />
</IconCircle>
<Heading :level="3" align="center">{{ t('clientOfferForm.noSchema.title') }}</Heading>
<Text tone="muted" align="center">
{{ t('clientOfferForm.noSchema.description', { name: productName }) }}
</Text>
<NuxtLink :to="localePath('/clientarea/offers/new')">
<Button variant="outline">{{ t('clientOfferForm.actions.chooseAnother') }}</Button>
</NuxtLink>
</Stack>
</Card>
<!-- Form -->
<Card v-else padding="lg">
<Stack gap="4">
<Stack gap="2">
<Heading :level="2">{{ productName }}</Heading>
<Text v-if="schemaDescription" tone="muted">{{ schemaDescription }}</Text>
</Stack>
<Stack gap="2">
<Text weight="semibold">{{ t('clientOfferForm.labels.location') }}</Text>
<select v-model="selectedAddressUuid" class="select select-bordered w-full">
<option v-if="!addresses.length" :value="null">
{{ t('clientOfferForm.labels.location_empty') }}
</option>
<option
v-for="address in addresses"
:key="address.uuid"
:value="address.uuid"
>
{{ address.name }} {{ address.address }}
</option>
</select>
</Stack>
<hr class="border-base-300" />
<!-- FormKit dynamic form -->
<FormKit
type="form"
:actions="false"
:config="formKitConfig"
@submit="handleSubmit"
>
<Stack gap="4">
<FormKitSchema :schema="formkitSchema" />
<Stack direction="row" gap="3" justify="end">
<Button
variant="outline"
type="button"
@click="navigateTo(localePath('/clientarea/offers/new'))"
>
{{ t('common.cancel') }}
</Button>
<Button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? t('clientOfferForm.actions.saving') : t('clientOfferForm.actions.save') }}
</Button>
</Stack>
</Stack>
</FormKit>
</Stack>
</Card>
<!-- Debug info -->
<Card v-if="isDev" padding="md" tone="muted">
<Stack gap="2">
<Text size="sm" weight="semibold">Debug Info</Text>
<Text size="sm" tone="muted">Product UUID: {{ productUuid }}</Text>
<Text size="sm" tone="muted">Product Name: {{ productName }}</Text>
<Text size="sm" tone="muted">Schema ID: {{ schemaId || t('clientOfferForm.debug.schema_missing') }}</Text>
<details>
<summary class="cursor-pointer text-sm text-base-content/70">FormKit Schema</summary>
<pre class="text-xs mt-2 p-2 bg-base-200 border border-base-300 rounded overflow-auto">{{ JSON.stringify(formkitSchema, null, 2) }}</pre>
</details>
</Stack>
</Card>
</Stack>
</template>
<script setup lang="ts">
import { FormKitSchema } from '@formkit/vue'
import type { FormKitSchemaNode } from '@formkit/core'
import { GetProductsDocument } from '~/composables/graphql/public/exchange-generated'
import { CreateOfferDocument } from '~/composables/graphql/team/exchange-generated'
import { GetTeamAddressesDocument } from '~/composables/graphql/team/teams-generated'
definePageMeta({
layout: 'topnav',
middleware: ['auth-oidc'],
validate: (route) => {
// Exclude 'new' from the dynamic route
return route.params.uuid !== 'new'
}
})
const { t } = useI18n()
const localePath = useLocalePath()
const route = useRoute()
const { execute, mutate } = useGraphQL()
const { getSchema, getEnums, schemaToFormKit } = useTerminus()
const { activeTeamId } = useActiveTeam()
// State
const isLoading = ref(true)
const hasError = ref(false)
const error = ref('')
const isSubmitting = ref(false)
const productUuid = computed(() => route.params.uuid as string)
const productName = ref<string>('')
const schemaId = ref<string | null>(null)
const schemaDescription = ref<string | null>(null)
const formkitSchema = ref<FormKitSchemaNode[]>([])
const addresses = ref<any[]>([])
const selectedAddressUuid = ref<string | null>(null)
const formKitConfig = {
classes: {
form: 'space-y-4',
label: 'text-sm font-semibold',
inner: 'w-full',
input: 'input input-bordered w-full',
textarea: 'textarea textarea-bordered w-full',
select: 'select select-bordered w-full',
help: 'text-sm text-base-content/60',
messages: 'text-error text-sm mt-1',
message: 'text-error text-sm',
},
}
const isDev = process.dev
const loadAddresses = async () => {
try {
const { data, error: addressesError } = await useServerQuery('offer-form-addresses', GetTeamAddressesDocument, {}, 'team', 'teams')
if (addressesError.value) throw addressesError.value
addresses.value = data.value?.teamAddresses || []
const defaultAddress = addresses.value.find((address: any) => address.isDefault)
selectedAddressUuid.value = defaultAddress?.uuid || addresses.value[0]?.uuid || null
} catch (err) {
console.error('Failed to load addresses:', err)
addresses.value = []
selectedAddressUuid.value = null
}
}
// Load data
const loadData = async () => {
try {
isLoading.value = true
hasError.value = false
// 1. Load product and get terminus_schema_id
const { data: productsData, error: productsError } = await useServerQuery('offer-form-products', GetProductsDocument, {}, 'public', 'exchange')
if (productsError.value) throw productsError.value
const products = productsData.value?.getProducts || []
const product = products.find((p: any) => p.uuid === productUuid.value)
if (!product) {
throw new Error(t('clientOfferForm.errors.productNotFound', { uuid: productUuid.value }))
}
productName.value = product.name
schemaId.value = product.terminusSchemaId || null
if (!schemaId.value) {
// No schema configured
isLoading.value = false
return
}
// 2. Load schema from TerminusDB
const terminusClass = await getSchema(schemaId.value)
if (!terminusClass) {
throw new Error(t('clientOfferForm.errors.schemaNotFound', { schema: schemaId.value }))
}
// Save description
schemaDescription.value = terminusClass['@documentation']?.['@comment'] || null
// 3. Load enums and convert to FormKit schema
const enums = await getEnums()
formkitSchema.value = schemaToFormKit(terminusClass, enums)
await loadAddresses()
} catch (err: any) {
hasError.value = true
error.value = err.message || t('clientOfferForm.error.load')
console.error('Load error:', err)
} finally {
isLoading.value = false
}
}
// Handle form submission
const handleSubmit = async (data: Record<string, unknown>) => {
try {
isSubmitting.value = true
if (!activeTeamId.value) {
throw new Error(t('clientOfferForm.error.load'))
}
const selectedAddress = addresses.value.find((address: any) => address.uuid === selectedAddressUuid.value)
if (!selectedAddress) {
throw new Error(t('clientOfferForm.error.save'))
}
const input = {
teamUuid: activeTeamId.value,
productUuid: productUuid.value,
productName: productName.value,
categoryName: undefined,
locationUuid: selectedAddress.uuid,
locationName: selectedAddress.name,
locationCountry: '',
locationCountryCode: selectedAddress.countryCode || '',
locationLatitude: selectedAddress.latitude,
locationLongitude: selectedAddress.longitude,
quantity: data.quantity || 0,
unit: data.unit || 'ton',
pricePerUnit: data.price_per_unit || data.pricePerUnit || null,
currency: data.currency || 'USD',
description: data.description || '',
validUntil: data.valid_until || data.validUntil || null,
terminusSchemaId: schemaId.value,
terminusPayload: JSON.stringify(data),
}
const result = await mutate(CreateOfferDocument, { input }, 'team', 'exchange')
if (!result.createOffer?.success) {
throw new Error(result.createOffer?.message || t('clientOfferForm.error.save'))
}
await navigateTo(localePath('/clientarea/offers'))
} catch (err: any) {
error.value = err.message || t('clientOfferForm.error.save')
hasError.value = true
} finally {
isSubmitting.value = false
}
}
await loadData()
</script>