Refactor duplicated site forms and requirements

This commit is contained in:
MatthieuTD
2025-09-25 12:01:28 +02:00
parent ac0687ac8f
commit a4840c454f
11 changed files with 545 additions and 496 deletions

View File

@@ -1,137 +1,50 @@
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body space-y-4">
<div class="flex items-center justify-between">
<h3 class="card-title text-lg">Familles de composants</h3>
<button type="button" class="btn btn-primary btn-sm" @click="addRequirement">
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
Ajouter une famille
</button>
</div>
<p class="text-sm text-gray-500">
Chaque ligne correspond à un groupe de composants attendus pour le type de machine. Sélectionnez le type de composant (famille), puis définissez le nombre minimal/maximal et si l'utilisateur peut créer un nouveau modèle lors de l'instanciation d'une machine.
</p>
<div v-if="requirements.length === 0" class="text-sm text-gray-500 bg-base-200/60 rounded-md p-4">
Aucun groupe configuré. Ajoutez votre première famille de composants.
</div>
<div
v-for="(requirement, index) in requirements"
:key="requirement.id || index"
class="border border-base-200 rounded-lg p-4 space-y-3"
>
<div class="flex items-start justify-between gap-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 flex-1">
<div class="form-control">
<label class="label">
<span class="label-text">Type de composant</span>
<span class="label-text-alt text-error">*</span>
</label>
<select
:value="requirement.typeComposantId || ''"
class="select select-bordered select-sm"
required
@change="updateRequirement(index, { typeComposantId: $event.target.value || null })"
>
<option value="">Sélectionner un type</option>
<option
v-for="type in componentTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Libellé</span>
<span class="label-text-alt text-xs">Optionnel</span>
</label>
<input
:value="requirement.label || ''"
type="text"
class="input input-bordered input-sm"
placeholder="Ex: Sangles principales"
@input="updateRequirement(index, { label: $event.target.value })"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Minimum requis</span>
</label>
<input
:value="requirement.minCount ?? 1"
type="number"
min="0"
class="input input-bordered input-sm"
@input="updateRequirement(index, { minCount: parseNumber($event.target.value) })"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Maximum autorisé</span>
<span class="label-text-alt text-xs">Laisser vide pour illimité</span>
</label>
<input
:value="requirement.maxCount ?? ''"
type="number"
min="0"
class="input input-bordered input-sm"
@input="updateRequirement(index, { maxCount: parseOptionalNumber($event.target.value) })"
/>
</div>
</div>
<button
type="button"
class="btn btn-square btn-error btn-sm"
@click="removeRequirement(index)"
>
<IconLucideTrash2 class="w-4 h-4" aria-hidden="true" />
</button>
</div>
<div class="flex flex-wrap items-center gap-4 text-sm">
<label class="flex items-center gap-2">
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="requirement.required ?? true"
@change="updateRequirement(index, { required: $event.target.checked })"
/>
Requis
</label>
<label class="flex items-center gap-2">
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="requirement.allowNewModels ?? true"
@change="updateRequirement(index, { allowNewModels: $event.target.checked })"
/>
Autoriser la création de nouveaux modèles lors de l'instanciation
</label>
</div>
</div>
</div>
</div>
<RequirementListEditor
v-model="requirements"
:type-options="componentTypes"
type-field="typeComposantId"
:labels="labels"
:default-requirement="createDefaultRequirement"
:required-fallback="true"
:min-fallback="1"
/>
</template>
<script setup>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideTrash2 from '~icons/lucide/trash-2'
import RequirementListEditor from '~/components/common/RequirementListEditor.vue'
import { useComponentTypes } from '~/composables/useComponentTypes'
type Requirement = Record<string, unknown> & {
id?: string | number
typeComposantId?: string | number | null
label?: string
minCount?: number | null
maxCount?: number | null
required?: boolean | null
allowNewModels?: boolean | null
}
type Labels = {
headerTitle: string
addButton: string
description: string
emptyState: string
typeSelectLabel: string
typePlaceholder: string
labelFieldLabel: string
labelFieldHelper: string
labelPlaceholder: string
minLabel: string
maxLabel: string
maxHelper: string
requiredLabel: string
allowNewModelsLabel: string
}
const props = defineProps({
modelValue: {
type: Array,
type: Array as () => Requirement[],
default: () => [],
},
})
@@ -142,50 +55,40 @@ const { componentTypes, loadComponentTypes } = useComponentTypes()
const requirements = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
set: (value: Requirement[]) => emit('update:modelValue', value),
})
const createDefaultRequirement = (): Requirement => ({
id: undefined,
typeComposantId: null,
label: '',
minCount: 1,
maxCount: null,
required: true,
allowNewModels: true,
})
const labels: Labels = {
headerTitle: 'Familles de composants',
addButton: 'Ajouter une famille',
description:
"Chaque ligne correspond à un groupe de composants attendus pour le type de machine. Sélectionnez le type de composant (famille), puis définissez le nombre minimal/maximal et si l'utilisateur peut créer un nouveau modèle lors de l'instanciation d'une machine.",
emptyState: 'Aucun groupe configuré. Ajoutez votre première famille de composants.',
typeSelectLabel: 'Type de composant',
typePlaceholder: 'Sélectionner un type',
labelFieldLabel: 'Libellé',
labelFieldHelper: 'Optionnel',
labelPlaceholder: 'Ex: Sangles principales',
minLabel: 'Minimum requis',
maxLabel: 'Maximum autorisé',
maxHelper: 'Laisser vide pour illimité',
requiredLabel: 'Requis',
allowNewModelsLabel: "Autoriser la création de nouveaux modèles lors de l'instanciation",
}
onMounted(async () => {
if (!componentTypes.value.length) {
await loadComponentTypes()
}
})
const addRequirement = () => {
requirements.value = [
...requirements.value,
{
id: undefined,
typeComposantId: null,
label: '',
minCount: 1,
maxCount: null,
required: true,
allowNewModels: true,
},
]
}
const removeRequirement = (index) => {
requirements.value = requirements.value.filter((_, i) => i !== index)
}
const updateRequirement = (index, patch) => {
requirements.value = requirements.value.map((item, i) =>
i === index ? { ...item, ...patch } : item
)
}
const parseNumber = (value) => {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
const parseOptionalNumber = (value) => {
if (value === '' || value === null || value === undefined) {
return null
}
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
</script>

View File

@@ -1,137 +1,50 @@
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body space-y-4">
<div class="flex items-center justify-between">
<h3 class="card-title text-lg">Pièces principales</h3>
<button type="button" class="btn btn-primary btn-sm" @click="addRequirement">
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
Ajouter un groupe
</button>
</div>
<p class="text-sm text-gray-500">
Configurez ici les familles de pièces principales attendues pour ce type de machine. Le nombre minimal/maximal est utilisé pour guider la création d'une machine.
</p>
<div v-if="requirements.length === 0" class="text-sm text-gray-500 bg-base-200/60 rounded-md p-4">
Aucun groupe configuré. Ajoutez votre première famille de pièces.
</div>
<div
v-for="(requirement, index) in requirements"
:key="requirement.id || index"
class="border border-base-200 rounded-lg p-4 space-y-3"
>
<div class="flex items-start justify-between gap-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 flex-1">
<div class="form-control">
<label class="label">
<span class="label-text">Type de pièce</span>
<span class="label-text-alt text-error">*</span>
</label>
<select
:value="requirement.typePieceId || ''"
class="select select-bordered select-sm"
required
@change="updateRequirement(index, { typePieceId: $event.target.value || null })"
>
<option value="">Sélectionner un type</option>
<option
v-for="type in pieceTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Libellé</span>
<span class="label-text-alt text-xs">Optionnel</span>
</label>
<input
:value="requirement.label || ''"
type="text"
class="input input-bordered input-sm"
placeholder="Ex: Vis principale"
@input="updateRequirement(index, { label: $event.target.value })"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Minimum requis</span>
</label>
<input
:value="requirement.minCount ?? 0"
type="number"
min="0"
class="input input-bordered input-sm"
@input="updateRequirement(index, { minCount: parseNumber($event.target.value) })"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Maximum autorisé</span>
<span class="label-text-alt text-xs">Laisser vide pour illimité</span>
</label>
<input
:value="requirement.maxCount ?? ''"
type="number"
min="0"
class="input input-bordered input-sm"
@input="updateRequirement(index, { maxCount: parseOptionalNumber($event.target.value) })"
/>
</div>
</div>
<button
type="button"
class="btn btn-square btn-error btn-sm"
@click="removeRequirement(index)"
>
<IconLucideTrash2 class="w-4 h-4" aria-hidden="true" />
</button>
</div>
<div class="flex flex-wrap items-center gap-4 text-sm">
<label class="flex items-center gap-2">
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="requirement.required ?? false"
@change="updateRequirement(index, { required: $event.target.checked })"
/>
Requis
</label>
<label class="flex items-center gap-2">
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="requirement.allowNewModels ?? true"
@change="updateRequirement(index, { allowNewModels: $event.target.checked })"
/>
Autoriser la création de nouveaux modèles lors de l'instanciation
</label>
</div>
</div>
</div>
</div>
<RequirementListEditor
v-model="requirements"
:type-options="pieceTypes"
type-field="typePieceId"
:labels="labels"
:default-requirement="createDefaultRequirement"
:required-fallback="false"
:min-fallback="0"
/>
</template>
<script setup>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideTrash2 from '~icons/lucide/trash-2'
import RequirementListEditor from '~/components/common/RequirementListEditor.vue'
import { usePieceTypes } from '~/composables/usePieceTypes'
type Requirement = Record<string, unknown> & {
id?: string | number
typePieceId?: string | number | null
label?: string
minCount?: number | null
maxCount?: number | null
required?: boolean | null
allowNewModels?: boolean | null
}
type Labels = {
headerTitle: string
addButton: string
description: string
emptyState: string
typeSelectLabel: string
typePlaceholder: string
labelFieldLabel: string
labelFieldHelper: string
labelPlaceholder: string
minLabel: string
maxLabel: string
maxHelper: string
requiredLabel: string
allowNewModelsLabel: string
}
const props = defineProps({
modelValue: {
type: Array,
type: Array as () => Requirement[],
default: () => [],
},
})
@@ -142,50 +55,40 @@ const { pieceTypes, loadPieceTypes } = usePieceTypes()
const requirements = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
set: (value: Requirement[]) => emit('update:modelValue', value),
})
const createDefaultRequirement = (): Requirement => ({
id: undefined,
typePieceId: null,
label: '',
minCount: 0,
maxCount: null,
required: false,
allowNewModels: true,
})
const labels: Labels = {
headerTitle: 'Pièces principales',
addButton: 'Ajouter un groupe',
description:
"Configurez ici les familles de pièces principales attendues pour ce type de machine. Le nombre minimal/maximal est utilisé pour guider la création d'une machine.",
emptyState: 'Aucun groupe configuré. Ajoutez votre première famille de pièces.',
typeSelectLabel: 'Type de pièce',
typePlaceholder: 'Sélectionner un type',
labelFieldLabel: 'Libellé',
labelFieldHelper: 'Optionnel',
labelPlaceholder: 'Ex: Vis principale',
minLabel: 'Minimum requis',
maxLabel: 'Maximum autorisé',
maxHelper: 'Laisser vide pour illimité',
requiredLabel: 'Requis',
allowNewModelsLabel: "Autoriser la création de nouveaux modèles lors de l'instanciation",
}
onMounted(async () => {
if (!pieceTypes.value.length) {
await loadPieceTypes()
}
})
const addRequirement = () => {
requirements.value = [
...requirements.value,
{
id: undefined,
typePieceId: null,
label: '',
minCount: 0,
maxCount: null,
required: false,
allowNewModels: true,
},
]
}
const removeRequirement = (index) => {
requirements.value = requirements.value.filter((_, i) => i !== index)
}
const updateRequirement = (index, patch) => {
requirements.value = requirements.value.map((item, i) =>
i === index ? { ...item, ...patch } : item
)
}
const parseNumber = (value) => {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
const parseOptionalNumber = (value) => {
if (value === '' || value === null || value === undefined) {
return null
}
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
</script>

View File

@@ -0,0 +1,246 @@
<template>
<div class="card bg-base-100 shadow-lg">
<div class="card-body space-y-4">
<div class="flex items-center justify-between">
<h3 class="card-title text-lg">{{ labels.headerTitle }}</h3>
<button type="button" class="btn btn-primary btn-sm" @click="addRequirement">
<IconLucidePlus class="w-4 h-4 mr-2" aria-hidden="true" />
{{ labels.addButton }}
</button>
</div>
<p class="text-sm text-gray-500">
{{ labels.description }}
</p>
<div v-if="requirements.length === 0" class="text-sm text-gray-500 bg-base-200/60 rounded-md p-4">
{{ labels.emptyState }}
</div>
<div
v-for="(requirement, index) in requirements"
:key="requirement.id || index"
class="border border-base-200 rounded-lg p-4 space-y-3"
>
<div class="flex items-start justify-between gap-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 flex-1">
<div class="form-control">
<label class="label">
<span class="label-text">{{ labels.typeSelectLabel }}</span>
<span class="label-text-alt text-error">*</span>
</label>
<select
:value="requirement[typeField] ?? ''"
class="select select-bordered select-sm"
required
@change="updateRequirement(index, { [typeField]: normalizeTypeValue($event.target.value) })"
>
<option value="">{{ labels.typePlaceholder }}</option>
<option
v-for="type in typeOptions"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{{ labels.labelFieldLabel }}</span>
<span v-if="labels.labelFieldHelper" class="label-text-alt text-xs">{{ labels.labelFieldHelper }}</span>
</label>
<input
:value="requirement.label ?? ''"
type="text"
class="input input-bordered input-sm"
:placeholder="labels.labelPlaceholder"
@input="updateRequirement(index, { label: $event.target.value })"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{{ labels.minLabel }}</span>
</label>
<input
:value="requirement.minCount ?? minFallback"
type="number"
min="0"
class="input input-bordered input-sm"
@input="updateRequirement(index, { minCount: parseNumber($event.target.value) })"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{{ labels.maxLabel }}</span>
<span v-if="labels.maxHelper" class="label-text-alt text-xs">{{ labels.maxHelper }}</span>
</label>
<input
:value="requirement.maxCount ?? ''"
type="number"
min="0"
class="input input-bordered input-sm"
@input="updateRequirement(index, { maxCount: parseOptionalNumber($event.target.value) })"
/>
</div>
</div>
<button
type="button"
class="btn btn-square btn-error btn-sm"
@click="removeRequirement(index)"
>
<IconLucideTrash2 class="w-4 h-4" aria-hidden="true" />
</button>
</div>
<div class="flex flex-wrap items-center gap-4 text-sm">
<label class="flex items-center gap-2">
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="(requirement.required ?? requiredFallback) === true"
@change="updateRequirement(index, { required: $event.target.checked })"
/>
{{ labels.requiredLabel }}
</label>
<label class="flex items-center gap-2">
<input
type="checkbox"
class="checkbox checkbox-sm"
:checked="(requirement.allowNewModels ?? allowNewModelsFallback) === true"
@change="updateRequirement(index, { allowNewModels: $event.target.checked })"
/>
{{ labels.allowNewModelsLabel }}
</label>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { PropType } from 'vue'
import IconLucidePlus from '~icons/lucide/plus'
import IconLucideTrash2 from '~icons/lucide/trash-2'
type Option = {
id: string | number
name: string
}
type Requirement = Record<string, unknown> & {
id?: string | number
label?: string
minCount?: number | null
maxCount?: number | null
required?: boolean | null
allowNewModels?: boolean | null
}
type Labels = {
headerTitle: string
addButton: string
description: string
emptyState: string
typeSelectLabel: string
typePlaceholder: string
labelFieldLabel: string
labelFieldHelper?: string
labelPlaceholder?: string
minLabel: string
maxLabel: string
maxHelper?: string
requiredLabel: string
allowNewModelsLabel: string
}
const props = defineProps({
modelValue: {
type: Array as PropType<Requirement[]>,
default: () => [],
},
typeOptions: {
type: Array as PropType<Option[]>,
default: () => [],
},
typeField: {
type: String,
required: true,
},
labels: {
type: Object as PropType<Labels>,
required: true,
},
defaultRequirement: {
type: Function as PropType<() => Requirement>,
required: true,
},
requiredFallback: {
type: Boolean,
default: false,
},
allowNewModelsFallback: {
type: Boolean,
default: true,
},
minFallback: {
type: Number,
default: 0,
},
})
const emit = defineEmits(['update:modelValue'])
const requirements = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
})
const addRequirement = () => {
requirements.value = [
...requirements.value,
props.defaultRequirement(),
]
}
const removeRequirement = (index: number) => {
requirements.value = requirements.value.filter((_, i) => i !== index)
}
const updateRequirement = (index: number, patch: Partial<Requirement>) => {
requirements.value = requirements.value.map((item, i) =>
i === index ? { ...item, ...patch } : item
)
}
const parseNumber = (value: string) => {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
const parseOptionalNumber = (value: string) => {
if (value === '' || value === null || value === undefined) {
return null
}
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
const normalizeTypeValue = (value: string) => {
if (!value) {
return null
}
return value
}
</script>
<!--
Éditeur générique de groupes de contraintes (pièces/composants) pour les types de machine.
Paramétrer les libellés et la structure via les props pour réutiliser ce bloc.
-->

View File

@@ -0,0 +1,98 @@
<template>
<div class="grid grid-cols-1 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Nom du contact</span>
</label>
<input
v-model="form.contactName"
type="text"
placeholder="Nom et prénom"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Téléphone</span>
</label>
<input
v-model="form.contactPhone"
type="tel"
placeholder="Ex: 06 00 00 00 00"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Adresse</span>
</label>
<input
v-model="form.contactAddress"
type="text"
placeholder="Adresse complète"
class="input input-bordered"
required
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Code postal</span>
</label>
<input
v-model="form.contactPostalCode"
type="text"
placeholder="Code postal"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Ville</span>
</label>
<input
v-model="form.contactCity"
type="text"
placeholder="Ville"
class="input input-bordered"
required
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { toRefs } from 'vue'
import type { PropType } from 'vue'
type SiteForm = {
contactName: string
contactPhone: string
contactAddress: string
contactPostalCode: string
contactCity: string
}
const props = defineProps({
form: {
type: Object as PropType<SiteForm>,
required: true,
},
})
const form = toRefs(props.form)
</script>
<!--
Bloc de formulaire partagé pour la saisie/édition des informations de contact d'un site.
Utilisation :
<SiteContactFormFields :form="siteForm" />
-->

View File

@@ -16,74 +16,7 @@
/>
</div>
<div class="grid grid-cols-1 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Nom du contact</span>
</label>
<input
v-model="form.contactName"
type="text"
placeholder="Nom et prénom"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Téléphone</span>
</label>
<input
v-model="form.contactPhone"
type="tel"
placeholder="Ex: 06 00 00 00 00"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Adresse</span>
</label>
<input
v-model="form.contactAddress"
type="text"
placeholder="Adresse complète"
class="input input-bordered"
required
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Code postal</span>
</label>
<input
v-model="form.contactPostalCode"
type="text"
placeholder="Code postal"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Ville</span>
</label>
<input
v-model="form.contactCity"
type="text"
placeholder="Ville"
class="input input-bordered"
required
/>
</div>
</div>
</div>
<SiteContactFormFields :form="props.site" />
<div class="modal-action">
<button type="button" class="btn" @click="emit('close')">
@@ -100,6 +33,7 @@
<script setup>
import { toRefs } from 'vue'
import SiteContactFormFields from '~/components/sites/SiteContactFormFields.vue'
const props = defineProps({
visible: {

View File

@@ -19,74 +19,7 @@
/>
</div>
<div class="grid grid-cols-1 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Nom du contact</span>
</label>
<input
v-model="form.contactName"
type="text"
placeholder="Nom et prénom"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Téléphone</span>
</label>
<input
v-model="form.contactPhone"
type="tel"
placeholder="Ex: 06 00 00 00 00"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Adresse</span>
</label>
<input
v-model="form.contactAddress"
type="text"
placeholder="Adresse complète"
class="input input-bordered"
required
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Code postal</span>
</label>
<input
v-model="form.contactPostalCode"
type="text"
placeholder="Code postal"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Ville</span>
</label>
<input
v-model="form.contactCity"
type="text"
placeholder="Ville"
class="input input-bordered"
required
/>
</div>
</div>
</div>
<SiteContactFormFields :form="props.form" />
<div class="border-t border-base-200 pt-4 space-y-4">
<div class="flex items-center justify-between">
@@ -163,6 +96,7 @@
<script setup>
import { computed, toRefs } from 'vue'
import DocumentUpload from '~/components/DocumentUpload.vue'
import SiteContactFormFields from '~/components/sites/SiteContactFormFields.vue'
const props = defineProps({
visible: {