WIP
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
class="text-xs text-base-content/50"
|
||||
>
|
||||
{{ constructeur.name }}
|
||||
<span v-if="supplierReferenceMap.get(constructeur.id)" class="opacity-70">({{ supplierReferenceMap.get(constructeur.id) }})</span>
|
||||
</span>
|
||||
<span v-if="displayProductName" class="badge badge-info badge-xs">
|
||||
{{ displayProductName }}
|
||||
@@ -102,6 +103,9 @@
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ constructeur.name }}
|
||||
<span v-if="supplierReferenceMap.get(constructeur.id)" class="text-sm text-base-content/60">
|
||||
— Réf. {{ supplierReferenceMap.get(constructeur.id) }}
|
||||
</span>
|
||||
<span v-if="formatConstructeurContact(constructeur)" class="text-xs text-base-content/50 block">
|
||||
{{ formatConstructeurContact(constructeur) }}
|
||||
</span>
|
||||
@@ -292,6 +296,7 @@ import {
|
||||
formatConstructeurContact as formatConstructeurContactSummary,
|
||||
resolveConstructeurs,
|
||||
uniqueConstructeurIds,
|
||||
parseConstructeurLinksFromApi,
|
||||
} from '~/shared/constructeurUtils'
|
||||
import {
|
||||
formatSize,
|
||||
@@ -391,23 +396,36 @@ const structurePieces = computed(() => allPieces.value.filter((p) => p._structur
|
||||
// --- Constructeurs ---
|
||||
const { constructeurs } = useConstructeurs()
|
||||
|
||||
const componentConstructeurIds = computed(() =>
|
||||
uniqueConstructeurIds(
|
||||
props.component,
|
||||
const componentConstructeurLinks = computed(() =>
|
||||
parseConstructeurLinksFromApi(
|
||||
Array.isArray(props.component.constructeurs) ? props.component.constructeurs : [],
|
||||
props.component.constructeur ? [props.component.constructeur] : [],
|
||||
),
|
||||
)
|
||||
|
||||
const componentConstructeursDisplay = computed(() =>
|
||||
resolveConstructeurs(
|
||||
componentConstructeurIds.value,
|
||||
Array.isArray(props.component.constructeurs) ? props.component.constructeurs : [],
|
||||
props.component.constructeur ? [props.component.constructeur] : [],
|
||||
constructeurs.value,
|
||||
),
|
||||
const supplierReferenceMap = computed(() => {
|
||||
const map = new Map()
|
||||
componentConstructeurLinks.value.forEach(l => {
|
||||
if (l.supplierReference) map.set(l.constructeurId, l.supplierReference)
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
const componentConstructeurIds = computed(() =>
|
||||
componentConstructeurLinks.value.map(l => l.constructeurId).filter(Boolean),
|
||||
)
|
||||
|
||||
const componentConstructeursDisplay = computed(() => {
|
||||
// Extract nested constructeur objects from link entries
|
||||
const linkConstructeurs = componentConstructeurLinks.value
|
||||
.filter(l => l.constructeur && l.constructeur.id)
|
||||
.map(l => l.constructeur)
|
||||
return resolveConstructeurs(
|
||||
componentConstructeurIds.value,
|
||||
linkConstructeurs,
|
||||
constructeurs.value,
|
||||
)
|
||||
})
|
||||
|
||||
const formatConstructeurContact = (constructeur) =>
|
||||
formatConstructeurContactSummary(constructeur)
|
||||
|
||||
|
||||
@@ -78,7 +78,9 @@ const getConstructeurContact = (link: ConstructeurLinkEntry): string => {
|
||||
|
||||
const updateReference = (index: number, value: string) => {
|
||||
const updated = [...props.modelValue]
|
||||
updated[index] = { ...updated[index], supplierReference: value || null }
|
||||
const entry = updated[index]
|
||||
if (!entry) return
|
||||
updated[index] = { ...entry, supplierReference: value || null }
|
||||
emit('update:modelValue', updated)
|
||||
}
|
||||
|
||||
@@ -86,6 +88,6 @@ const removeLink = (index: number) => {
|
||||
const removed = props.modelValue[index]
|
||||
const updated = props.modelValue.filter((_, i) => i !== index)
|
||||
emit('update:modelValue', updated)
|
||||
emit('remove', removed.constructeurId)
|
||||
if (removed) emit('remove', removed.constructeurId)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<IconLucideEye v-else class="w-5 h-5 mr-2" aria-hidden="true" />
|
||||
{{ isEditMode ? 'Voir détails' : 'Modifier' }}
|
||||
</button>
|
||||
<NuxtLink :to="backLink" class="btn btn-ghost btn-sm md:btn-md">
|
||||
<button type="button" class="btn btn-ghost btn-sm md:btn-md" @click="goBack">
|
||||
Retour au catalogue
|
||||
</NuxtLink>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -26,7 +26,9 @@
|
||||
import IconLucideSquarePen from '~icons/lucide/square-pen'
|
||||
import IconLucideEye from '~icons/lucide/eye'
|
||||
|
||||
defineProps<{
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
isEditMode: boolean
|
||||
@@ -37,4 +39,13 @@ defineProps<{
|
||||
defineEmits<{
|
||||
'toggle-edit': []
|
||||
}>()
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
}
|
||||
else {
|
||||
navigateTo(props.backLink)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -50,6 +50,9 @@
|
||||
class="badge badge-outline badge-sm"
|
||||
>
|
||||
{{ constructeur.name }}
|
||||
<span v-if="supplierReferenceMap.get(constructeur.id)" class="text-xs opacity-60 ml-0.5">
|
||||
({{ supplierReferenceMap.get(constructeur.id) }})
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<span v-if="pieceData.prix" class="badge badge-primary badge-sm">{{ pieceData.prix }}€</span>
|
||||
@@ -122,6 +125,9 @@
|
||||
>
|
||||
<span class="font-medium">
|
||||
{{ constructeur.name }}
|
||||
<span v-if="supplierReferenceMap.get(constructeur.id)" class="text-sm font-normal text-base-content/60">
|
||||
— Réf. {{ supplierReferenceMap.get(constructeur.id) }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="formatConstructeurContact(constructeur)"
|
||||
@@ -283,6 +289,7 @@ import {
|
||||
formatConstructeurContact as formatConstructeurContactSummary,
|
||||
resolveConstructeurs,
|
||||
uniqueConstructeurIds,
|
||||
parseConstructeurLinksFromApi,
|
||||
} from '~/shared/constructeurUtils'
|
||||
import {
|
||||
resolveFieldId,
|
||||
@@ -393,23 +400,36 @@ const toggleCollapse = () => {
|
||||
// --- Constructeurs ---
|
||||
const { constructeurs } = useConstructeurs()
|
||||
|
||||
const pieceConstructeurIds = computed(() =>
|
||||
uniqueConstructeurIds(
|
||||
props.piece,
|
||||
const pieceConstructeurLinks = computed(() =>
|
||||
parseConstructeurLinksFromApi(
|
||||
Array.isArray(props.piece.constructeurs) ? props.piece.constructeurs : [],
|
||||
props.piece.constructeur ? [props.piece.constructeur] : [],
|
||||
),
|
||||
)
|
||||
|
||||
const pieceConstructeursDisplay = computed(() =>
|
||||
resolveConstructeurs(
|
||||
pieceConstructeurIds.value,
|
||||
Array.isArray(props.piece.constructeurs) ? props.piece.constructeurs : [],
|
||||
props.piece.constructeur ? [props.piece.constructeur] : [],
|
||||
constructeurs.value,
|
||||
),
|
||||
const supplierReferenceMap = computed(() => {
|
||||
const map = new Map()
|
||||
pieceConstructeurLinks.value.forEach(l => {
|
||||
if (l.supplierReference) map.set(l.constructeurId, l.supplierReference)
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
const pieceConstructeurIds = computed(() =>
|
||||
pieceConstructeurLinks.value.map(l => l.constructeurId).filter(Boolean),
|
||||
)
|
||||
|
||||
const pieceConstructeursDisplay = computed(() => {
|
||||
// Extract nested constructeur objects from link entries
|
||||
const linkConstructeurs = pieceConstructeurLinks.value
|
||||
.filter(l => l.constructeur && l.constructeur.id)
|
||||
.map(l => l.constructeur)
|
||||
return resolveConstructeurs(
|
||||
pieceConstructeurIds.value,
|
||||
linkConstructeurs,
|
||||
constructeurs.value,
|
||||
)
|
||||
})
|
||||
|
||||
const formatConstructeurContact = (constructeur) =>
|
||||
formatConstructeurContactSummary(constructeur)
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
<IconLucidePrinter class="w-5 h-5 mr-2" aria-hidden="true" />
|
||||
Imprimer
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost btn-sm md:btn-md" @click="goBack">
|
||||
Retour aux machines
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -41,6 +44,8 @@ import IconLucideSquarePen from '~icons/lucide/square-pen'
|
||||
import IconLucideEye from '~icons/lucide/eye'
|
||||
import IconLucidePrinter from '~icons/lucide/printer'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
isEditMode: boolean
|
||||
@@ -50,4 +55,13 @@ defineEmits<{
|
||||
'toggle-edit': []
|
||||
'open-print': []
|
||||
}>()
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
}
|
||||
else {
|
||||
navigateTo('/machines')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{{ machineReference }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isEditMode || hasMachineConstructeur" class="form-control">
|
||||
<div v-if="isEditMode || hasMachineConstructeur" class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text">Fournisseur</span>
|
||||
</label>
|
||||
@@ -70,23 +70,15 @@
|
||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||
@update:modelValue="$emit('update:constructeur-ids', $event)"
|
||||
/>
|
||||
<div v-else class="border border-base-300 rounded-btn bg-base-200 px-4 py-2 min-h-12 flex items-center">
|
||||
<div v-if="machineConstructeursDisplay.length" class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="constructeur in machineConstructeursDisplay"
|
||||
:key="constructeur.id"
|
||||
class="badge badge-ghost gap-1"
|
||||
>
|
||||
{{ constructeur.name }}
|
||||
<span
|
||||
v-if="formatConstructeurContactSummary(constructeur)"
|
||||
class="text-xs opacity-60"
|
||||
>
|
||||
· {{ formatConstructeurContactSummary(constructeur) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="text-base-content/50">Non défini</span>
|
||||
<ConstructeurLinksTable
|
||||
v-if="constructeurLinks.length"
|
||||
:model-value="constructeurLinks"
|
||||
:readonly="!isEditMode"
|
||||
@update:model-value="$emit('update:constructeur-links', $event)"
|
||||
@remove="$emit('remove-constructeur-link', $event)"
|
||||
/>
|
||||
<div v-else-if="!isEditMode" class="border border-base-300 rounded-btn bg-base-200 px-4 py-2 min-h-12 flex items-center">
|
||||
<span class="text-base-content/50">Non défini</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,12 +180,11 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
||||
import ConstructeurLinksTable from '~/components/ConstructeurLinksTable.vue'
|
||||
import MachineCustomFieldDefEditor from '~/components/machine/MachineCustomFieldDefEditor.vue'
|
||||
import {
|
||||
formatConstructeurContact as formatConstructeurContactSummary,
|
||||
} from '~/shared/constructeurUtils'
|
||||
import { formatCustomFieldValue } from '~/shared/utils/customFieldUtils'
|
||||
import { useMachineCustomFieldDefs } from '~/composables/useMachineCustomFieldDefs'
|
||||
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||
|
||||
const props = defineProps<{
|
||||
isEditMode: boolean
|
||||
@@ -205,6 +196,7 @@ const props = defineProps<{
|
||||
machineConstructeurIds: string[]
|
||||
machineConstructeursDisplay: any[]
|
||||
hasMachineConstructeur: boolean
|
||||
constructeurLinks: ConstructeurLinkEntry[]
|
||||
visibleCustomFields: any[]
|
||||
getMachineFieldId: (fieldName: string) => string
|
||||
machineId: string
|
||||
@@ -216,6 +208,8 @@ const emit = defineEmits<{
|
||||
'update:machine-reference': [value: string]
|
||||
'update:machine-site-id': [value: string]
|
||||
'update:constructeur-ids': [ids: unknown]
|
||||
'update:constructeur-links': [links: ConstructeurLinkEntry[]]
|
||||
'remove-constructeur-link': [constructeurId: string]
|
||||
'set-custom-field-value': [field: any, value: unknown]
|
||||
'custom-fields-saved': []
|
||||
}>()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||
import { extractCollection } from '~/shared/utils/apiHelpers'
|
||||
|
||||
type EntityType = 'machine' | 'piece' | 'composant' | 'product'
|
||||
|
||||
@@ -25,7 +26,7 @@ const ENTITY_PLURALS: Record<EntityType, string> = {
|
||||
}
|
||||
|
||||
export function useConstructeurLinks() {
|
||||
const { get, post, patch, del } = useApi()
|
||||
const { get, post, patch, delete: del } = useApi()
|
||||
|
||||
const fetchLinks = async (
|
||||
entityType: EntityType,
|
||||
@@ -34,11 +35,11 @@ export function useConstructeurLinks() {
|
||||
const endpoint = ENDPOINTS[entityType]
|
||||
const key = ENTITY_KEYS[entityType]
|
||||
const plural = ENTITY_PLURALS[entityType]
|
||||
const result = await get(`${endpoint}?${key}=/api/${plural}/${entityId}`)
|
||||
const url = `${endpoint}?${key}=/api/${plural}/${entityId}`
|
||||
const result = await get(url)
|
||||
if (!result.success || !result.data) return []
|
||||
|
||||
const data = result.data as Record<string, any>
|
||||
const members = data['hydra:member'] ?? (Array.isArray(data) ? data : [])
|
||||
const members = extractCollection(result.data)
|
||||
if (!Array.isArray(members)) return []
|
||||
|
||||
return members.map((link: any) => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
||||
import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'
|
||||
import { useUrlState } from './useUrlState'
|
||||
import type { DataTableSort, DataTablePagination, DataTableColumnFilters, SortDirection } from '~/shared/types/dataTable'
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface UseDataTableOptions {
|
||||
persistToUrl?: boolean
|
||||
/** Extra URL state params for page-specific filters */
|
||||
extraParams?: Record<string, { default: string | number; type?: 'string' | 'number' }>
|
||||
/** Column filter keys to persist in URL (prefixed with `f.` in query string) */
|
||||
columnFilterKeys?: string[]
|
||||
}
|
||||
|
||||
export interface UseDataTableReturn {
|
||||
@@ -56,6 +58,7 @@ export function useDataTable(
|
||||
searchDebounceMs = 300,
|
||||
persistToUrl = true,
|
||||
extraParams = {},
|
||||
columnFilterKeys = [],
|
||||
} = options
|
||||
|
||||
let searchTerm: Ref<string>
|
||||
@@ -64,6 +67,7 @@ export function useDataTable(
|
||||
let currentPage: Ref<number>
|
||||
let itemsPerPage: Ref<number>
|
||||
const filters: Record<string, Ref<string | number>> = {}
|
||||
const columnFilterRefs: Record<string, Ref<string>> = {}
|
||||
|
||||
if (persistToUrl) {
|
||||
const paramDefs: Record<string, { default: string | number; type?: 'string' | 'number'; debounce?: number }> = {
|
||||
@@ -75,6 +79,10 @@ export function useDataTable(
|
||||
...extraParams,
|
||||
}
|
||||
|
||||
for (const key of columnFilterKeys) {
|
||||
paramDefs[`f.${key}`] = { default: '', debounce: 300 }
|
||||
}
|
||||
|
||||
const state = useUrlState(paramDefs, {
|
||||
onRestore: () => deps.fetchData(),
|
||||
})
|
||||
@@ -88,6 +96,10 @@ export function useDataTable(
|
||||
for (const key of Object.keys(extraParams)) {
|
||||
filters[key] = (state as Record<string, Ref<string | number>>)[key]!
|
||||
}
|
||||
|
||||
for (const key of columnFilterKeys) {
|
||||
columnFilterRefs[key] = (state as Record<string, Ref<string>>)[`f.${key}`]!
|
||||
}
|
||||
}
|
||||
else {
|
||||
searchTerm = ref('')
|
||||
@@ -137,8 +149,31 @@ export function useDataTable(
|
||||
deps.fetchData()
|
||||
}
|
||||
|
||||
// Column filters
|
||||
const columnFilters = ref<DataTableColumnFilters>({})
|
||||
// Column filters — seed from URL-persisted refs
|
||||
const initialColumnFilters: DataTableColumnFilters = {}
|
||||
for (const [key, r] of Object.entries(columnFilterRefs)) {
|
||||
if (r.value) initialColumnFilters[key] = r.value
|
||||
}
|
||||
const columnFilters = ref<DataTableColumnFilters>(initialColumnFilters)
|
||||
|
||||
// Sync columnFilters → URL refs
|
||||
if (persistToUrl && columnFilterKeys.length > 0) {
|
||||
watch(columnFilters, (val) => {
|
||||
for (const key of columnFilterKeys) {
|
||||
columnFilterRefs[key]!.value = val[key] || ''
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// Sync URL refs → columnFilters (back/forward navigation)
|
||||
for (const key of columnFilterKeys) {
|
||||
watch(columnFilterRefs[key]!, (urlVal) => {
|
||||
const current = columnFilters.value[key] || ''
|
||||
if (current !== urlVal) {
|
||||
columnFilters.value = { ...columnFilters.value, [key]: urlVal }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleColumnFiltersChange = (newFilters: DataTableColumnFilters) => {
|
||||
columnFilters.value = newFilters
|
||||
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
resolveConstructeurs,
|
||||
uniqueConstructeurIds,
|
||||
formatConstructeurContact as formatConstructeurContactSummary,
|
||||
parseConstructeurLinksFromApi,
|
||||
constructeurIdsFromLinks,
|
||||
} from '~/shared/constructeurUtils'
|
||||
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||
import { useMachineDetailDocuments } from '~/composables/useMachineDetailDocuments'
|
||||
import { useMachineDetailCustomFields } from '~/composables/useMachineDetailCustomFields'
|
||||
import { useMachineDetailHierarchy } from '~/composables/useMachineDetailHierarchy'
|
||||
@@ -64,6 +68,11 @@ export function useMachineDetailData(machineId: string) {
|
||||
const printAreaRef = ref<HTMLElement | null>(null)
|
||||
const saving = ref(false)
|
||||
|
||||
// Constructeur links
|
||||
const { fetchLinks, syncLinks } = useConstructeurLinks()
|
||||
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||
const originalConstructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||
|
||||
// Machine fields
|
||||
const machineName = ref('')
|
||||
const machineReference = ref('')
|
||||
@@ -78,20 +87,15 @@ export function useMachineDetailData(machineId: string) {
|
||||
})
|
||||
|
||||
const machineConstructeursDisplay = computed(() => {
|
||||
const ids = uniqueConstructeurIds(
|
||||
machineConstructeurIds.value,
|
||||
(machine.value as AnyRecord)?.constructeurIds,
|
||||
(machine.value as AnyRecord)?.constructeurs,
|
||||
(machine.value as AnyRecord)?.constructeur,
|
||||
)
|
||||
const ids = machineConstructeurIds.value
|
||||
if (!ids.length) return [] as any[]
|
||||
// Extract nested constructeur objects from link entries as candidate pool
|
||||
const linkConstructeurs = constructeurLinks.value
|
||||
.filter(l => l.constructeur && l.constructeur.id)
|
||||
.map(l => l.constructeur!) as any[]
|
||||
return resolveConstructeurs(
|
||||
ids,
|
||||
Array.isArray((machine.value as AnyRecord)?.constructeurs)
|
||||
? ((machine.value as AnyRecord).constructeurs as any[])
|
||||
: [],
|
||||
(machine.value as AnyRecord)?.constructeur
|
||||
? [(machine.value as AnyRecord).constructeur as any]
|
||||
: [],
|
||||
linkConstructeurs,
|
||||
constructeurs.value as any,
|
||||
) as any[]
|
||||
})
|
||||
@@ -235,11 +239,12 @@ export function useMachineDetailData(machineId: string) {
|
||||
if (machine.value) {
|
||||
machineName.value = (machine.value.name as string) || ''
|
||||
machineReference.value = (machine.value.reference as string) || ''
|
||||
machineConstructeurIds.value = uniqueConstructeurIds(
|
||||
machine.value.constructeurIds,
|
||||
machine.value.constructeurs,
|
||||
machine.value.constructeur,
|
||||
)
|
||||
// Parse constructeur links from structure response
|
||||
const rawLinks = Array.isArray(machine.value.constructeurs) ? machine.value.constructeurs as any[] : []
|
||||
const parsed = parseConstructeurLinksFromApi(rawLinks)
|
||||
constructeurLinks.value = parsed
|
||||
originalConstructeurLinks.value = parsed.map(l => ({ ...l }))
|
||||
machineConstructeurIds.value = constructeurIdsFromLinks(parsed)
|
||||
machineSiteId.value = (machine.value.siteId as string) || (machine.value.site as AnyRecord)?.id as string || ''
|
||||
}
|
||||
}
|
||||
@@ -269,6 +274,8 @@ export function useMachineDetailData(machineId: string) {
|
||||
machineReference,
|
||||
machineSiteId,
|
||||
machineConstructeurIds,
|
||||
constructeurLinks,
|
||||
originalConstructeurLinks,
|
||||
machineDocumentsLoaded,
|
||||
machineComponentLinks,
|
||||
machinePieceLinks,
|
||||
@@ -284,6 +291,7 @@ export function useMachineDetailData(machineId: string) {
|
||||
updatePieceApi,
|
||||
apiPatch,
|
||||
toast,
|
||||
syncLinks,
|
||||
})
|
||||
|
||||
// UI methods
|
||||
@@ -338,6 +346,8 @@ export function useMachineDetailData(machineId: string) {
|
||||
const cancelEdition = () => {
|
||||
initMachineFields()
|
||||
syncMachineCustomFields()
|
||||
constructeurLinks.value = originalConstructeurLinks.value.map(l => ({ ...l }))
|
||||
machineConstructeurIds.value = constructeurIdsFromLinks(constructeurLinks.value)
|
||||
isEditMode.value = false
|
||||
}
|
||||
|
||||
@@ -467,6 +477,7 @@ export function useMachineDetailData(machineId: string) {
|
||||
// Machine fields
|
||||
machineName, machineReference, machineSiteId, machineConstructeurIds, machineConstructeurId,
|
||||
machineConstructeursDisplay, machineConstructeurContact, hasMachineConstructeur,
|
||||
constructeurLinks, originalConstructeurLinks,
|
||||
sites,
|
||||
|
||||
// UI state
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import { uniqueConstructeurIds, constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
||||
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||
|
||||
type AnyRecord = Record<string, unknown>
|
||||
|
||||
@@ -15,6 +16,8 @@ export interface UseMachineDetailUpdatesDeps {
|
||||
machineReference: Ref<string>
|
||||
machineSiteId: Ref<string>
|
||||
machineConstructeurIds: Ref<string[]>
|
||||
constructeurLinks: Ref<ConstructeurLinkEntry[]>
|
||||
originalConstructeurLinks: Ref<ConstructeurLinkEntry[]>
|
||||
machineDocumentsLoaded: Ref<boolean>
|
||||
machineComponentLinks: Ref<AnyRecord[]>
|
||||
machinePieceLinks: Ref<AnyRecord[]>
|
||||
@@ -35,6 +38,12 @@ export interface UseMachineDetailUpdatesDeps {
|
||||
updatePieceApi: (id: string, data: any) => Promise<unknown>
|
||||
apiPatch: (endpoint: string, data?: unknown) => Promise<any>
|
||||
toast: { showInfo: (msg: string) => void }
|
||||
syncLinks: (
|
||||
entityType: 'machine' | 'piece' | 'composant' | 'product',
|
||||
entityId: string,
|
||||
originalLinks: ConstructeurLinkEntry[],
|
||||
formLinks: ConstructeurLinkEntry[],
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
||||
@@ -44,6 +53,8 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
||||
machineReference,
|
||||
machineSiteId,
|
||||
machineConstructeurIds,
|
||||
constructeurLinks,
|
||||
originalConstructeurLinks,
|
||||
machineComponentLinks,
|
||||
machinePieceLinks,
|
||||
applyMachineLinks,
|
||||
@@ -56,19 +67,16 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
||||
updatePieceApi,
|
||||
apiPatch,
|
||||
toast,
|
||||
syncLinks,
|
||||
} = deps
|
||||
|
||||
const updateMachineInfo = async () => {
|
||||
if (!machine.value) return
|
||||
try {
|
||||
const cIds = uniqueConstructeurIds(machineConstructeurIds.value)
|
||||
machineConstructeurIds.value = cIds
|
||||
|
||||
const result: any = await updateMachineApi(machine.value.id as string, {
|
||||
name: machineName.value,
|
||||
reference: machineReference.value,
|
||||
siteId: machineSiteId.value || undefined,
|
||||
constructeurIds: cIds,
|
||||
} as any)
|
||||
if (result.success) {
|
||||
const machinePayload =
|
||||
@@ -82,11 +90,6 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
||||
documents: machinePayload.documents || machine.value.documents || [],
|
||||
customFieldValues: machinePayload.customFieldValues || machine.value.customFieldValues || [],
|
||||
}
|
||||
machineConstructeurIds.value = uniqueConstructeurIds(
|
||||
machine.value!.constructeurIds,
|
||||
machine.value!.constructeurs,
|
||||
machine.value!.constructeur,
|
||||
)
|
||||
const linksApplied = applyMachineLinks(result.data)
|
||||
if (linksApplied && machine.value) {
|
||||
machine.value.componentLinks = machineComponentLinks.value
|
||||
@@ -95,6 +98,9 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
||||
loadProductDocuments().catch(() => {})
|
||||
}
|
||||
}
|
||||
// Sync constructeur links after entity save
|
||||
await syncLinks('machine', machine.value!.id as string, originalConstructeurLinks.value, constructeurLinks.value)
|
||||
originalConstructeurLinks.value = constructeurLinks.value.map(l => ({ ...l }))
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour de la machine:', error)
|
||||
}
|
||||
@@ -209,7 +215,13 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
||||
}
|
||||
|
||||
const handleMachineConstructeurChange = (value: unknown) => {
|
||||
machineConstructeurIds.value = uniqueConstructeurIds(value)
|
||||
const newIds = uniqueConstructeurIds(value)
|
||||
machineConstructeurIds.value = newIds
|
||||
// Sync constructeurLinks: keep existing entries, add new ones
|
||||
const existingMap = new Map(constructeurLinks.value.map(l => [l.constructeurId, l]))
|
||||
constructeurLinks.value = newIds.map(id =>
|
||||
existingMap.get(id) ?? { constructeurId: id, supplierReference: null },
|
||||
)
|
||||
}
|
||||
|
||||
const editComponent = () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from './useToast'
|
||||
import { useApi, type ApiResponse } from './useApi'
|
||||
import { buildConstructeurRequestPayload } from '~/shared/constructeurUtils'
|
||||
import { extractRelationId, normalizeRelationIds } from '~/shared/apiRelations'
|
||||
import { extractCollection } from '~/shared/utils/apiHelpers'
|
||||
|
||||
@@ -92,7 +91,7 @@ export function useMachines() {
|
||||
const createMachine = async (machineData: Partial<Machine>): Promise<ApiResponse> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(machineData))
|
||||
const normalizedPayload = normalizeRelationIds(machineData as Record<string, unknown>)
|
||||
const result = await post('/machines', normalizedPayload)
|
||||
if (result.success) {
|
||||
const createdMachine = normalizeMachineResponse(result.data) ||
|
||||
@@ -116,7 +115,7 @@ export function useMachines() {
|
||||
const updateMachineData = async (id: string, machineData: Partial<Machine>): Promise<ApiResponse> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const normalizedPayload = normalizeRelationIds(buildConstructeurRequestPayload(machineData))
|
||||
const normalizedPayload = normalizeRelationIds(machineData as Record<string, unknown>)
|
||||
const result = await patch(`/machines/${id}`, normalizedPayload)
|
||||
if (result.success) {
|
||||
const updatedMachine = normalizeMachineResponse(result.data) ||
|
||||
|
||||
@@ -166,6 +166,7 @@ const table = useDataTable(
|
||||
status: { default: 'open' },
|
||||
entityType: { default: '' },
|
||||
},
|
||||
columnFilterKeys: ['entity'],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||
|
||||
const table = useDataTable(
|
||||
{ fetchData: fetchComposants },
|
||||
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true },
|
||||
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true, columnFilterKeys: ['typeComposant'] },
|
||||
)
|
||||
|
||||
const columns = [
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
:machine-constructeur-ids="d.machineConstructeurIds.value"
|
||||
:machine-constructeurs-display="d.machineConstructeursDisplay.value"
|
||||
:has-machine-constructeur="d.hasMachineConstructeur.value"
|
||||
:constructeur-links="d.constructeurLinks.value"
|
||||
:visible-custom-fields="d.visibleMachineCustomFields.value"
|
||||
:get-machine-field-id="d.getMachineFieldId"
|
||||
:machine-id="machineId"
|
||||
@@ -72,6 +73,8 @@
|
||||
@update:machine-reference="d.machineReference.value = $event"
|
||||
@update:machine-site-id="d.machineSiteId.value = $event"
|
||||
@update:constructeur-ids="d.handleMachineConstructeurChange"
|
||||
@update:constructeur-links="d.constructeurLinks.value = $event"
|
||||
@remove-constructeur-link="handleRemoveConstructeurLink"
|
||||
@set-custom-field-value="d.setMachineCustomFieldValue"
|
||||
@custom-fields-saved="() => { d.loadMachineData(); refreshVersions() }"
|
||||
/>
|
||||
@@ -196,9 +199,9 @@
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-base-content mb-1">Machine non trouvée</h3>
|
||||
<p class="text-sm text-base-content/50 mb-6">La machine avec l'ID "{{ machineId }}" n'existe pas ou a été supprimée.</p>
|
||||
<NuxtLink to="/machines" class="btn btn-primary">
|
||||
<button type="button" class="btn btn-primary" @click="$router.back()">
|
||||
Retour aux machines
|
||||
</NuxtLink>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -280,6 +283,11 @@ const openAddModal = (kind) => {
|
||||
addModalOpen.value = true
|
||||
}
|
||||
|
||||
const handleRemoveConstructeurLink = (constructeurId) => {
|
||||
const ids = d.machineConstructeurIds.value.filter(id => id !== constructeurId)
|
||||
d.handleMachineConstructeurChange(ids)
|
||||
}
|
||||
|
||||
const handleAddEntity = async (entityId) => {
|
||||
if (addModalKind.value === 'component') {
|
||||
await d.addComponentLink(entityId)
|
||||
|
||||
@@ -120,11 +120,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useMachines } from '~/composables/useMachines'
|
||||
import { useSites } from '~/composables/useSites'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { humanizeError } from '~/shared/utils/errorMessages'
|
||||
import { useUrlState } from '~/composables/useUrlState'
|
||||
import IconLucidePlus from '~icons/lucide/plus'
|
||||
import IconLucideFactory from '~icons/lucide/factory'
|
||||
import IconLucideMapPin from '~icons/lucide/map-pin'
|
||||
@@ -135,8 +136,28 @@ const { machines, loading, loadMachines, deleteMachine } = useMachines()
|
||||
const { sites, loadSites } = useSites()
|
||||
const toast = useToast()
|
||||
|
||||
const urlState = useUrlState({
|
||||
q: { default: '', debounce: 300 },
|
||||
sites: { default: '' },
|
||||
})
|
||||
|
||||
const searchQuery = urlState.q
|
||||
const selectedSites = reactive(new Set())
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Sync URL → selectedSites on load and back/forward
|
||||
watch(urlState.sites, (val) => {
|
||||
selectedSites.clear()
|
||||
if (val) {
|
||||
for (const id of String(val).split(',')) {
|
||||
if (id) selectedSites.add(id)
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Sync selectedSites → URL
|
||||
watch(() => [...selectedSites], (ids) => {
|
||||
urlState.sites.value = ids.join(',')
|
||||
})
|
||||
|
||||
// Enrichir les machines avec les objets site complets
|
||||
const enrichedMachines = computed(() => {
|
||||
|
||||
@@ -112,6 +112,8 @@ const loadCategory = async () => {
|
||||
category: response.category,
|
||||
notes: response.notes ?? response.description ?? '',
|
||||
structure: (response.structure as PieceModelStructure | null) ?? undefined,
|
||||
referenceFormula: response.referenceFormula ?? null,
|
||||
requiredFieldsForReference: response.requiredFieldsForReference ?? null,
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -171,7 +171,7 @@ const { pieceTypes, loadPieceTypes } = usePieceTypes()
|
||||
|
||||
const table = useDataTable(
|
||||
{ fetchData: fetchPieces },
|
||||
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true },
|
||||
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true, columnFilterKeys: ['typePiece'] },
|
||||
)
|
||||
|
||||
const columns = [
|
||||
|
||||
@@ -91,6 +91,10 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ConstructeurLinksTable
|
||||
v-if="constructeurLinks.length"
|
||||
v-model="constructeurLinks"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
@@ -213,6 +217,7 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from '#imports'
|
||||
import ConstructeurSelect from '~/components/ConstructeurSelect.vue'
|
||||
import ConstructeurLinksTable from '~/components/ConstructeurLinksTable.vue'
|
||||
import DocumentUpload from '~/components/DocumentUpload.vue'
|
||||
import ProductSelect from '~/components/ProductSelect.vue'
|
||||
import SearchSelect from '~/components/common/SearchSelect.vue'
|
||||
@@ -222,8 +227,11 @@ import { useToast } from '~/composables/useToast'
|
||||
import { humanizeError } from '~/shared/utils/errorMessages'
|
||||
import { useCustomFields } from '~/composables/useCustomFields'
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
import { useConstructeurLinks } from '~/composables/useConstructeurLinks'
|
||||
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||
import { constructeurIdsFromLinks } from '~/shared/constructeurUtils'
|
||||
import type { ConstructeurLinkEntry } from '~/shared/constructeurUtils'
|
||||
import type { PieceModelStructure } from '~/shared/types/inventory'
|
||||
import type { ModelType } from '~/services/modelTypes'
|
||||
import {
|
||||
@@ -255,6 +263,8 @@ const { createPiece } = usePieces()
|
||||
const toast = useToast()
|
||||
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
||||
const { uploadDocuments } = useDocuments()
|
||||
const { syncLinks } = useConstructeurLinks()
|
||||
const { getConstructeurById } = useConstructeurs()
|
||||
const { canEdit } = usePermissions()
|
||||
|
||||
const initialTypeId = ref<string>(typeof route.query.typeId === 'string' ? route.query.typeId : '')
|
||||
@@ -267,6 +277,7 @@ const creationForm = reactive({
|
||||
constructeurIds: [] as string[],
|
||||
prix: '' as string,
|
||||
})
|
||||
const constructeurLinks = ref<ConstructeurLinkEntry[]>([])
|
||||
const productSelections = ref<(string | null)[]>([])
|
||||
|
||||
const lastSuggestedName = ref('')
|
||||
@@ -380,6 +391,7 @@ const clearCreationForm = () => {
|
||||
creationForm.description = ''
|
||||
creationForm.reference = ''
|
||||
creationForm.constructeurIds = []
|
||||
constructeurLinks.value = []
|
||||
creationForm.prix = ''
|
||||
productSelections.value = []
|
||||
lastSuggestedName.value = ''
|
||||
@@ -411,8 +423,6 @@ const submitCreation = async () => {
|
||||
payload.reference = reference
|
||||
}
|
||||
|
||||
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
||||
|
||||
const normalizedProductIds = collectNormalizedProductIds(
|
||||
productRequirementEntries.value,
|
||||
productSelections.value,
|
||||
@@ -448,6 +458,10 @@ const submitCreation = async () => {
|
||||
],
|
||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||
)
|
||||
// Sync constructeur links after creation
|
||||
if (constructeurLinks.value.length) {
|
||||
await syncLinks('piece', createdPiece.id, [], constructeurLinks.value)
|
||||
}
|
||||
if (selectedDocuments.value.length && createdPiece.id) {
|
||||
uploadingDocuments.value = true
|
||||
const uploadResult = await uploadDocuments(
|
||||
@@ -478,6 +492,26 @@ const submitCreation = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync constructeurIds → constructeurLinks when IDs are added via ConstructeurSelect
|
||||
watch(
|
||||
() => creationForm.constructeurIds,
|
||||
(ids) => {
|
||||
const currentIds = new Set(constructeurLinks.value.map(l => l.constructeurId))
|
||||
for (const id of ids) {
|
||||
if (!currentIds.has(id)) {
|
||||
const resolved = getConstructeurById(id)
|
||||
constructeurLinks.value.push({
|
||||
constructeurId: id,
|
||||
constructeur: resolved ? { id: resolved.id, name: resolved.name } : null,
|
||||
supplierReference: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
constructeurLinks.value = constructeurLinks.value.filter(l => ids.includes(l.constructeurId))
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadPieceTypes()
|
||||
})
|
||||
|
||||
@@ -175,7 +175,7 @@ const toast = useToast()
|
||||
|
||||
const table = useDataTable(
|
||||
{ fetchData: fetchProducts },
|
||||
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true },
|
||||
{ defaultSort: 'name', defaultDirection: 'asc', defaultPerPage: 20, persistToUrl: true, columnFilterKeys: ['typeProduct'] },
|
||||
)
|
||||
|
||||
const errorMessage = computed(() => (typeof error.value === 'string' && error.value.length ? error.value : null))
|
||||
|
||||
@@ -139,49 +139,3 @@ export const formatConstructeurContact = (
|
||||
return [constructeur.email, phone].filter(Boolean).join(' • ');
|
||||
};
|
||||
|
||||
export const buildConstructeurRequestPayload = <T extends Record<string, any>>(
|
||||
payload: T,
|
||||
): T & { constructeurs?: string[] } => {
|
||||
const collected = new Set(uniqueConstructeurIds(
|
||||
payload?.constructeurIds,
|
||||
payload?.constructeurId,
|
||||
payload?.constructeur,
|
||||
payload?.constructeurs,
|
||||
));
|
||||
|
||||
if (!collected.size) {
|
||||
const fallbackLists = [
|
||||
payload?.constructeurIds,
|
||||
payload?.constructeurs,
|
||||
];
|
||||
fallbackLists.forEach((list) => {
|
||||
if (!Array.isArray(list)) {
|
||||
return;
|
||||
}
|
||||
list.forEach((item) => {
|
||||
if (typeof item === 'string') {
|
||||
const id = toStringId(item);
|
||||
if (id) {
|
||||
collected.add(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isObject(item) && typeof item.id === 'string') {
|
||||
collected.add(item.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const ids = Array.from(collected);
|
||||
|
||||
const next = { ...payload } as Record<string, any>;
|
||||
delete next.constructeurId;
|
||||
delete next.constructeur;
|
||||
delete next.constructeurs;
|
||||
delete next.constructeurIds;
|
||||
|
||||
next.constructeurs = ids.map((id) => `/api/constructeurs/${id}`);
|
||||
|
||||
return next as T & { constructeurs?: string[] };
|
||||
};
|
||||
|
||||
@@ -52,10 +52,10 @@ export default defineNuxtConfig({
|
||||
appVersion: appVersion,
|
||||
apiTimeout: process.env.NUXT_PUBLIC_API_TIMEOUT || '30000',
|
||||
requestTimeout: process.env.NUXT_PUBLIC_REQUEST_TIMEOUT || '10000',
|
||||
enableDebug: process.env.NUXT_PUBLIC_ENABLE_DEBUG || 'true',
|
||||
enableDebug: process.env.NUXT_PUBLIC_ENABLE_DEBUG || 'false',
|
||||
enableAnalytics: process.env.NUXT_PUBLIC_ENABLE_ANALYTICS || 'false',
|
||||
csrfToken: process.env.NUXT_PUBLIC_CSRF_TOKEN || '',
|
||||
logLevel: process.env.NUXT_PUBLIC_LOG_LEVEL || 'debug'
|
||||
logLevel: process.env.NUXT_PUBLIC_LOG_LEVEL || 'warn'
|
||||
}
|
||||
},
|
||||
vite: {
|
||||
|
||||
Reference in New Issue
Block a user