Initial commit from monorepo
This commit is contained in:
281
app/pages/clientarea/offers/[uuid].vue
Normal file
281
app/pages/clientarea/offers/[uuid].vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<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({
|
||||
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>
|
||||
Reference in New Issue
Block a user