Fix fournisseur handling across catalog flows
This commit is contained in:
@@ -109,6 +109,7 @@
|
|||||||
v-if="isEditMode"
|
v-if="isEditMode"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:model-value="componentConstructeurIds"
|
:model-value="componentConstructeurIds"
|
||||||
|
:initial-options="componentConstructeursDisplay"
|
||||||
@update:model-value="handleConstructeurChange"
|
@update:model-value="handleConstructeurChange"
|
||||||
/>
|
/>
|
||||||
<div v-else class="input input-bordered input-sm bg-base-200">
|
<div v-else class="input input-bordered input-sm bg-base-200">
|
||||||
|
|||||||
@@ -142,13 +142,22 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'Sélectionner ou créer un fournisseur...',
|
default: 'Sélectionner ou créer un fournisseur...',
|
||||||
},
|
},
|
||||||
|
initialOptions: {
|
||||||
|
type: Array as PropType<ConstructeurSummary[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string[]): void
|
(e: 'update:modelValue', value: string[]): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { constructeurs, searchConstructeurs, createConstructeur } = useConstructeurs()
|
const {
|
||||||
|
constructeurs,
|
||||||
|
searchConstructeurs,
|
||||||
|
createConstructeur,
|
||||||
|
ensureConstructeurs,
|
||||||
|
} = useConstructeurs()
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref('')
|
||||||
const openDropdown = ref(false)
|
const openDropdown = ref(false)
|
||||||
const openCreateModal = ref(false)
|
const openCreateModal = ref(false)
|
||||||
@@ -168,8 +177,15 @@ const uniqueOptions = (items: ConstructeurSummary[] = []) => {
|
|||||||
return Array.from(seen.values())
|
return Array.from(seen.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedInitialOptions = computed(() =>
|
||||||
|
uniqueOptions((props.initialOptions as ConstructeurSummary[]) || []),
|
||||||
|
)
|
||||||
|
|
||||||
const applyOptions = (items: ConstructeurSummary[] = []) => {
|
const applyOptions = (items: ConstructeurSummary[] = []) => {
|
||||||
const normalized = uniqueOptions(items)
|
const normalized = uniqueOptions([
|
||||||
|
...normalizedInitialOptions.value,
|
||||||
|
...items,
|
||||||
|
])
|
||||||
const limited = normalized.slice(0, 10)
|
const limited = normalized.slice(0, 10)
|
||||||
|
|
||||||
selectedIds.value.forEach((id) => {
|
selectedIds.value.forEach((id) => {
|
||||||
@@ -186,7 +202,10 @@ const applyOptions = (items: ConstructeurSummary[] = []) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
options.value = uniqueOptions(limited)
|
options.value = uniqueOptions([
|
||||||
|
...normalizedInitialOptions.value,
|
||||||
|
...limited,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const createForm = ref({
|
const createForm = ref({
|
||||||
@@ -197,6 +216,9 @@ const createForm = ref({
|
|||||||
|
|
||||||
const optionLookup = computed(() => {
|
const optionLookup = computed(() => {
|
||||||
const map = new Map<string, ConstructeurSummary>()
|
const map = new Map<string, ConstructeurSummary>()
|
||||||
|
normalizedInitialOptions.value.forEach((item) => {
|
||||||
|
map.set(item.id, item)
|
||||||
|
})
|
||||||
constructeurs.value.forEach((item: ConstructeurSummary) => {
|
constructeurs.value.forEach((item: ConstructeurSummary) => {
|
||||||
map.set(item.id, item)
|
map.set(item.id, item)
|
||||||
})
|
})
|
||||||
@@ -336,7 +358,10 @@ watch(
|
|||||||
}
|
}
|
||||||
const missing = ids.some((id) => !optionLookup.value.get(id))
|
const missing = ids.some((id) => !optionLookup.value.get(id))
|
||||||
if (missing) {
|
if (missing) {
|
||||||
await ensureOptionsLoaded(true)
|
const fetched = await ensureConstructeurs(ids)
|
||||||
|
if (fetched.length) {
|
||||||
|
applyOptions([...options.value, ...fetched])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -353,6 +378,14 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
normalizedInitialOptions,
|
||||||
|
() => {
|
||||||
|
applyOptions(options.value)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('click', clickHandler)
|
window.addEventListener('click', clickHandler)
|
||||||
ensureOptionsLoaded()
|
ensureOptionsLoaded()
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
v-else
|
v-else
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:model-value="pieceConstructeurIds"
|
:model-value="pieceConstructeurIds"
|
||||||
|
:initial-options="pieceConstructeursDisplay"
|
||||||
placeholder="Sélectionner un ou plusieurs fournisseurs..."
|
placeholder="Sélectionner un ou plusieurs fournisseurs..."
|
||||||
@update:model-value="handleConstructeurChange"
|
@update:model-value="handleConstructeurChange"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,10 +29,27 @@ export function useApi () {
|
|||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
let data = null
|
||||||
|
if (response.status !== 204) {
|
||||||
|
const contentType = response.headers.get('content-type') || ''
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
const text = await response.text()
|
||||||
|
data = text ? JSON.parse(text) : null
|
||||||
|
} else {
|
||||||
|
const text = await response.text()
|
||||||
|
data = text || null
|
||||||
|
}
|
||||||
|
}
|
||||||
return { success: true, data }
|
return { success: true, data }
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json().catch(() => ({}))
|
const contentType = response.headers.get('content-type') || ''
|
||||||
|
let errorData = {}
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
errorData = await response.json().catch(() => ({}))
|
||||||
|
} else {
|
||||||
|
const text = await response.text().catch(() => '')
|
||||||
|
errorData = text ? { message: text } : {}
|
||||||
|
}
|
||||||
const errorMessage = errorData.message || `Erreur ${response.status}: ${response.statusText}`
|
const errorMessage = errorData.message || `Erreur ${response.status}: ${response.statusText}`
|
||||||
showError(errorMessage)
|
showError(errorMessage)
|
||||||
return { success: false, error: errorMessage, status: response.status }
|
return { success: false, error: errorMessage, status: response.status }
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { buildConstructeurRequestPayload } from '~/shared/constructeurUtils'
|
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
|
import { useConstructeurs } from './useConstructeurs'
|
||||||
|
|
||||||
const composants = ref([])
|
const composants = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -9,13 +10,38 @@ const loading = ref(false)
|
|||||||
export function useComposants () {
|
export function useComposants () {
|
||||||
const { showSuccess, showError, showInfo } = useToast()
|
const { showSuccess, showError, showInfo } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
|
|
||||||
|
const withResolvedConstructeurs = async (composant) => {
|
||||||
|
if (!composant || typeof composant !== 'object') {
|
||||||
|
return composant
|
||||||
|
}
|
||||||
|
const ids = uniqueConstructeurIds(
|
||||||
|
composant.constructeurIds,
|
||||||
|
composant.constructeurs,
|
||||||
|
composant.constructeur,
|
||||||
|
)
|
||||||
|
const hasConstructeurs =
|
||||||
|
Array.isArray(composant.constructeurs) && composant.constructeurs.length > 0
|
||||||
|
|
||||||
|
if (ids.length && !hasConstructeurs) {
|
||||||
|
const resolved = await ensureConstructeurs(ids)
|
||||||
|
if (resolved.length) {
|
||||||
|
composant.constructeurs = resolved
|
||||||
|
composant.constructeurIds = ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return composant
|
||||||
|
}
|
||||||
|
|
||||||
const loadComposants = async () => {
|
const loadComposants = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await get('/composants')
|
const result = await get('/composants')
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
composants.value = result.data
|
const items = Array.isArray(result.data) ? result.data : []
|
||||||
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
|
composants.value = enrichedItems
|
||||||
showInfo(`Chargement de ${composants.value.length} composant(s) réussi`)
|
showInfo(`Chargement de ${composants.value.length} composant(s) réussi`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -30,7 +56,8 @@ const loadComposants = async () => {
|
|||||||
try {
|
try {
|
||||||
const result = await post('/composants', buildConstructeurRequestPayload(composantData))
|
const result = await post('/composants', buildConstructeurRequestPayload(composantData))
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
composants.value.push(result.data)
|
const enriched = await withResolvedConstructeurs(result.data)
|
||||||
|
composants.value.push(enriched)
|
||||||
const displayName = result.data?.name
|
const displayName = result.data?.name
|
||||||
|| composantData?.definition?.name
|
|| composantData?.definition?.name
|
||||||
|| composantData?.name
|
|| composantData?.name
|
||||||
@@ -51,7 +78,7 @@ const loadComposants = async () => {
|
|||||||
try {
|
try {
|
||||||
const result = await patch(`/composants/${id}`, buildConstructeurRequestPayload(composantData))
|
const result = await patch(`/composants/${id}`, buildConstructeurRequestPayload(composantData))
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const updated = result.data
|
const updated = await withResolvedConstructeurs(result.data)
|
||||||
const index = composants.value.findIndex(comp => comp.id === id)
|
const index = composants.value.findIndex(comp => comp.id === id)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
composants.value[index] = updated
|
composants.value[index] = updated
|
||||||
|
|||||||
@@ -5,6 +5,42 @@ import { useToast } from './useToast'
|
|||||||
const constructeurs = ref([])
|
const constructeurs = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const uniqueConstructeurs = (items = []) => {
|
||||||
|
const map = new Map()
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item && typeof item === 'object' && typeof item.id === 'string') {
|
||||||
|
map.set(item.id, item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Array.from(map.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeIds = (ids = []) => {
|
||||||
|
if (!Array.isArray(ids)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
ids
|
||||||
|
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
||||||
|
.filter((value) => value.length > 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertConstructeurs = (items = []) => {
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const merged = uniqueConstructeurs([...constructeurs.value, ...items])
|
||||||
|
constructeurs.value = merged
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIndexedConstructeur = (id) =>
|
||||||
|
constructeurs.value.find((item) => item && item.id === id) || null
|
||||||
|
|
||||||
|
const pendingFetches = new Map()
|
||||||
|
|
||||||
export function useConstructeurs () {
|
export function useConstructeurs () {
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
const { showSuccess, showError } = useToast()
|
const { showSuccess, showError } = useToast()
|
||||||
@@ -15,7 +51,8 @@ export function useConstructeurs () {
|
|||||||
const query = search ? `?search=${encodeURIComponent(search)}` : ''
|
const query = search ? `?search=${encodeURIComponent(search)}` : ''
|
||||||
const result = await get(`/constructeurs${query}`)
|
const result = await get(`/constructeurs${query}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
constructeurs.value = result.data
|
const items = Array.isArray(result.data) ? result.data : []
|
||||||
|
constructeurs.value = uniqueConstructeurs(items)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -35,7 +72,7 @@ export function useConstructeurs () {
|
|||||||
try {
|
try {
|
||||||
const result = await post('/constructeurs', data)
|
const result = await post('/constructeurs', data)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
constructeurs.value = [result.data, ...constructeurs.value]
|
upsertConstructeurs([result.data])
|
||||||
showSuccess(`Fournisseur "${result.data.name}" créé`)
|
showSuccess(`Fournisseur "${result.data.name}" créé`)
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
showError(result.error)
|
showError(result.error)
|
||||||
@@ -50,15 +87,65 @@ export function useConstructeurs () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ensureConstructeurs = async (ids = []) => {
|
||||||
|
const normalizedIds = normalizeIds(ids)
|
||||||
|
if (!normalizedIds.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const collected = []
|
||||||
|
const missing = []
|
||||||
|
normalizedIds.forEach((id) => {
|
||||||
|
const existing = getIndexedConstructeur(id)
|
||||||
|
if (existing) {
|
||||||
|
collected.push(existing)
|
||||||
|
} else {
|
||||||
|
missing.push(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (missing.length) {
|
||||||
|
const fetchTasks = missing.map((id) => {
|
||||||
|
const cached = pendingFetches.get(id)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
const task = get(`/constructeurs/${id}`)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.success && result.data) {
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Erreur lors du chargement du fournisseur:', error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
pendingFetches.delete(id)
|
||||||
|
})
|
||||||
|
pendingFetches.set(id, task)
|
||||||
|
return task
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetched = await Promise.all(fetchTasks)
|
||||||
|
const validFetched = fetched.filter((item) => item && item.id)
|
||||||
|
if (validFetched.length) {
|
||||||
|
upsertConstructeurs(validFetched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedIds
|
||||||
|
.map((id) => getIndexedConstructeur(id))
|
||||||
|
.filter((item) => Boolean(item))
|
||||||
|
}
|
||||||
|
|
||||||
const updateConstructeur = async (id, data) => {
|
const updateConstructeur = async (id, data) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await patch(`/constructeurs/${id}`, data)
|
const result = await patch(`/constructeurs/${id}`, data)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const index = constructeurs.value.findIndex(item => item.id === id)
|
upsertConstructeurs([result.data])
|
||||||
if (index !== -1) {
|
|
||||||
constructeurs.value[index] = result.data
|
|
||||||
}
|
|
||||||
showSuccess(`Fournisseur "${result.data.name}" mis à jour`)
|
showSuccess(`Fournisseur "${result.data.name}" mis à jour`)
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
showError(result.error)
|
showError(result.error)
|
||||||
@@ -93,7 +180,7 @@ export function useConstructeurs () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getConstructeurById = id => constructeurs.value.find(item => item.id === id)
|
const getConstructeurById = (id) => getIndexedConstructeur(id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
constructeurs,
|
constructeurs,
|
||||||
@@ -103,6 +190,7 @@ export function useConstructeurs () {
|
|||||||
createConstructeur,
|
createConstructeur,
|
||||||
updateConstructeur,
|
updateConstructeur,
|
||||||
deleteConstructeur,
|
deleteConstructeur,
|
||||||
getConstructeurById
|
getConstructeurById,
|
||||||
|
ensureConstructeurs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
import { buildConstructeurRequestPayload } from '~/shared/constructeurUtils'
|
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
|
import { useConstructeurs } from './useConstructeurs'
|
||||||
|
|
||||||
const pieces = ref([])
|
const pieces = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -9,13 +10,38 @@ const loading = ref(false)
|
|||||||
export function usePieces () {
|
export function usePieces () {
|
||||||
const { showSuccess, showError, showInfo } = useToast()
|
const { showSuccess, showError, showInfo } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
|
|
||||||
|
const withResolvedConstructeurs = async (piece) => {
|
||||||
|
if (!piece || typeof piece !== 'object') {
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
const ids = uniqueConstructeurIds(
|
||||||
|
piece.constructeurIds,
|
||||||
|
piece.constructeurs,
|
||||||
|
piece.constructeur,
|
||||||
|
)
|
||||||
|
const hasConstructeurs =
|
||||||
|
Array.isArray(piece.constructeurs) && piece.constructeurs.length > 0
|
||||||
|
|
||||||
|
if (ids.length && !hasConstructeurs) {
|
||||||
|
const resolved = await ensureConstructeurs(ids)
|
||||||
|
if (resolved.length) {
|
||||||
|
piece.constructeurs = resolved
|
||||||
|
piece.constructeurIds = ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
const loadPieces = async () => {
|
const loadPieces = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await get('/pieces')
|
const result = await get('/pieces')
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
pieces.value = result.data
|
const items = Array.isArray(result.data) ? result.data : []
|
||||||
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
|
pieces.value = enrichedItems
|
||||||
showInfo(`Chargement de ${pieces.value.length} pièce(s) réussi`)
|
showInfo(`Chargement de ${pieces.value.length} pièce(s) réussi`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -30,7 +56,8 @@ export function usePieces () {
|
|||||||
try {
|
try {
|
||||||
const result = await post('/pieces', buildConstructeurRequestPayload(pieceData))
|
const result = await post('/pieces', buildConstructeurRequestPayload(pieceData))
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
pieces.value.push(result.data)
|
const enriched = await withResolvedConstructeurs(result.data)
|
||||||
|
pieces.value.push(enriched)
|
||||||
const displayName = result.data?.name
|
const displayName = result.data?.name
|
||||||
|| pieceData?.definition?.name
|
|| pieceData?.definition?.name
|
||||||
|| pieceData?.name
|
|| pieceData?.name
|
||||||
@@ -51,7 +78,7 @@ export function usePieces () {
|
|||||||
try {
|
try {
|
||||||
const result = await patch(`/pieces/${id}`, buildConstructeurRequestPayload(pieceData))
|
const result = await patch(`/pieces/${id}`, buildConstructeurRequestPayload(pieceData))
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const updated = result.data
|
const updated = await withResolvedConstructeurs(result.data)
|
||||||
const index = pieces.value.findIndex(piece => piece.id === id)
|
const index = pieces.value.findIndex(piece => piece.id === id)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
pieces.value[index] = updated
|
pieces.value[index] = updated
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useToast } from './useToast'
|
import { useToast } from './useToast'
|
||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
|
import { buildConstructeurRequestPayload, uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
|
import { useConstructeurs } from './useConstructeurs'
|
||||||
|
|
||||||
const products = ref([])
|
const products = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@@ -26,6 +28,29 @@ const replaceInCache = (item) => {
|
|||||||
export function useProducts () {
|
export function useProducts () {
|
||||||
const { showError } = useToast()
|
const { showError } = useToast()
|
||||||
const { get, post, patch, delete: del } = useApi()
|
const { get, post, patch, delete: del } = useApi()
|
||||||
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
|
|
||||||
|
const withResolvedConstructeurs = async (product) => {
|
||||||
|
if (!product || typeof product !== 'object') {
|
||||||
|
return product
|
||||||
|
}
|
||||||
|
const ids = uniqueConstructeurIds(
|
||||||
|
product.constructeurIds,
|
||||||
|
product.constructeurs,
|
||||||
|
product.constructeur,
|
||||||
|
)
|
||||||
|
const hasConstructeurs =
|
||||||
|
Array.isArray(product.constructeurs) && product.constructeurs.length > 0
|
||||||
|
|
||||||
|
if (ids.length && !hasConstructeurs) {
|
||||||
|
const resolved = await ensureConstructeurs(ids)
|
||||||
|
if (resolved.length) {
|
||||||
|
product.constructeurs = resolved
|
||||||
|
product.constructeurIds = ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return product
|
||||||
|
}
|
||||||
|
|
||||||
const loadProducts = async (options = {}) => {
|
const loadProducts = async (options = {}) => {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
@@ -47,7 +72,8 @@ export function useProducts () {
|
|||||||
const result = await get('/products?limit=100')
|
const result = await get('/products?limit=100')
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const items = Array.isArray(result.data?.items) ? result.data.items : []
|
const items = Array.isArray(result.data?.items) ? result.data.items : []
|
||||||
products.value = items
|
const enrichedItems = await Promise.all(items.map((item) => withResolvedConstructeurs(item)))
|
||||||
|
products.value = enrichedItems
|
||||||
total.value = typeof result.data?.total === 'number' ? result.data.total : items.length
|
total.value = typeof result.data?.total === 'number' ? result.data.total : items.length
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
@@ -67,12 +93,14 @@ export function useProducts () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createProduct = async (payload) => {
|
const createProduct = async (payload) => {
|
||||||
|
const normalizedPayload = buildConstructeurRequestPayload(payload)
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const result = await post('/products', payload)
|
const result = await post('/products', normalizedPayload)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const added = replaceInCache(result.data)
|
const enriched = await withResolvedConstructeurs(result.data)
|
||||||
|
const added = replaceInCache(enriched)
|
||||||
if (added) {
|
if (added) {
|
||||||
total.value += 1
|
total.value += 1
|
||||||
}
|
}
|
||||||
@@ -93,12 +121,14 @@ export function useProducts () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateProduct = async (id, payload) => {
|
const updateProduct = async (id, payload) => {
|
||||||
|
const normalizedPayload = buildConstructeurRequestPayload(payload)
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const result = await patch(`/products/${id}`, payload)
|
const result = await patch(`/products/${id}`, normalizedPayload)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
replaceInCache(result.data)
|
const enriched = await withResolvedConstructeurs(result.data)
|
||||||
|
replaceInCache(enriched)
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
error.value = result.error
|
error.value = result.error
|
||||||
showError(result.error)
|
showError(result.error)
|
||||||
@@ -141,9 +171,10 @@ export function useProducts () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getProduct = async (id, options = {}) => {
|
const getProduct = async (id, options = {}) => {
|
||||||
if (!options.force) {
|
const shouldForce = !!options.force
|
||||||
|
if (!shouldForce) {
|
||||||
const cached = products.value.find((product) => product.id === id)
|
const cached = products.value.find((product) => product.id === id)
|
||||||
if (cached) {
|
if (cached && Array.isArray(cached.constructeurs) && cached.constructeurs.length > 0) {
|
||||||
return { success: true, data: cached }
|
return { success: true, data: cached }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +182,9 @@ export function useProducts () {
|
|||||||
try {
|
try {
|
||||||
const result = await get(`/products/${id}`)
|
const result = await get(`/products/${id}`)
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
replaceInCache(result.data)
|
const enriched = await withResolvedConstructeurs(result.data)
|
||||||
|
replaceInCache(enriched)
|
||||||
|
return { success: true, data: enriched }
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -102,6 +102,7 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||||
|
:initial-options="component?.constructeurs || []"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,6 +404,7 @@ import { useCustomFields } from '~/composables/useCustomFields'
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
import { formatStructurePreview, normalizeStructureForEditor } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
import type { ComponentModelStructure } from '~/shared/types/inventory'
|
||||||
@@ -432,6 +434,7 @@ const router = useRouter()
|
|||||||
const { get } = useApi()
|
const { get } = useApi()
|
||||||
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
const { componentTypes, loadComponentTypes } = useComponentTypes()
|
||||||
const { updateComposant } = useComposants()
|
const { updateComposant } = useComposants()
|
||||||
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments()
|
const { loadDocumentsByComponent, uploadDocuments, deleteDocument } = useDocuments()
|
||||||
@@ -658,6 +661,9 @@ watch(
|
|||||||
currentComponent.constructeur ? [currentComponent.constructeur] : [],
|
currentComponent.constructeur ? [currentComponent.constructeur] : [],
|
||||||
)
|
)
|
||||||
editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : ''
|
editionForm.prix = currentComponent.prix !== null && currentComponent.prix !== undefined ? String(currentComponent.prix) : ''
|
||||||
|
if (editionForm.constructeurIds.length) {
|
||||||
|
void ensureConstructeurs(editionForm.constructeurIds)
|
||||||
|
}
|
||||||
|
|
||||||
customFieldInputs.value = buildCustomFieldInputs(
|
customFieldInputs.value = buildCustomFieldInputs(
|
||||||
currentStructure,
|
currentStructure,
|
||||||
|
|||||||
@@ -144,6 +144,7 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
:key="machine.value?.id"
|
:key="machine.value?.id"
|
||||||
:model-value="machineConstructeurIds"
|
:model-value="machineConstructeurIds"
|
||||||
|
:initial-options="machineConstructeursDisplay"
|
||||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||||
@update:modelValue="handleMachineConstructeurChange"
|
@update:modelValue="handleMachineConstructeurChange"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -93,25 +93,48 @@
|
|||||||
<th class="w-24">Aperçu</th>
|
<th class="w-24">Aperçu</th>
|
||||||
<th>Nom</th>
|
<th>Nom</th>
|
||||||
<th>Référence</th>
|
<th>Référence</th>
|
||||||
|
<th>Fournisseurs</th>
|
||||||
<th>Type de pièce</th>
|
<th>Type de pièce</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="piece in visiblePieces" :key="piece.id">
|
<tr v-for="row in pieceRows" :key="row.piece.id">
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<DocumentThumbnail
|
<DocumentThumbnail
|
||||||
:document="resolvePrimaryDocument(piece)"
|
:document="resolvePrimaryDocument(row.piece)"
|
||||||
:alt="resolvePreviewAlt(piece)"
|
:alt="resolvePreviewAlt(row.piece)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ piece.name || 'Pièce sans nom' }}</td>
|
<td>{{ row.piece.name || 'Pièce sans nom' }}</td>
|
||||||
<td>{{ piece.reference || '—' }}</td>
|
<td>{{ row.piece.reference || '—' }}</td>
|
||||||
<td>{{ resolvePieceType(piece) }}</td>
|
<td>
|
||||||
|
<div
|
||||||
|
v-if="row.suppliers.visible.length"
|
||||||
|
class="flex max-w-[14rem] flex-wrap items-center gap-1"
|
||||||
|
:title="row.suppliers.tooltip"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="supplier in row.suppliers.visible"
|
||||||
|
:key="supplier"
|
||||||
|
class="badge badge-ghost badge-sm whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{{ supplier }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="row.suppliers.overflow"
|
||||||
|
class="badge badge-outline badge-sm"
|
||||||
|
>
|
||||||
|
+{{ row.suppliers.overflow }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ resolvePieceType(row.piece) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="`/pieces/${piece.id}/edit`"
|
:to="`/pieces/${row.piece.id}/edit`"
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
>
|
>
|
||||||
Modifier
|
Modifier
|
||||||
@@ -120,7 +143,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-error btn-xs"
|
class="btn btn-error btn-xs"
|
||||||
:disabled="loadingPieces"
|
:disabled="loadingPieces"
|
||||||
@click="handleDeletePiece(piece)"
|
@click="handleDeletePiece(row.piece)"
|
||||||
>
|
>
|
||||||
Supprimer
|
Supprimer
|
||||||
</button>
|
</button>
|
||||||
@@ -193,6 +216,88 @@ const resolvePieceType = (piece: Record<string, any>) => {
|
|||||||
return '—'
|
return '—'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_VISIBLE_SUPPLIERS = 3
|
||||||
|
|
||||||
|
const resolvePieceSuppliers = (piece: Record<string, any>) => {
|
||||||
|
const names: string[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
|
||||||
|
const pushName = (maybeName: unknown) => {
|
||||||
|
if (typeof maybeName !== 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const normalized = maybeName.trim().replace(/\s+/g, ' ')
|
||||||
|
if (!normalized.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const key = normalized.toLowerCase()
|
||||||
|
if (seen.has(key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen.add(key)
|
||||||
|
names.push(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectConstructeurs = (value: unknown): void => {
|
||||||
|
if (!value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(collectConstructeurs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
pushName(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
const record = value as Record<string, any>
|
||||||
|
pushName(record?.name ?? record?.label ?? record?.companyName ?? record?.company ?? null)
|
||||||
|
if (record?.constructeur) {
|
||||||
|
collectConstructeurs(record.constructeur)
|
||||||
|
}
|
||||||
|
if (Array.isArray(record?.constructeurs)) {
|
||||||
|
collectConstructeurs(record.constructeurs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectFromLabel = (value: unknown): void => {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value
|
||||||
|
.split(/[,;\\/•·|]+/)
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach(pushName)
|
||||||
|
}
|
||||||
|
|
||||||
|
collectConstructeurs(piece?.constructeurs)
|
||||||
|
collectConstructeurs(piece?.constructeur)
|
||||||
|
collectConstructeurs(piece?.product?.constructeurs)
|
||||||
|
collectConstructeurs(piece?.product?.constructeur)
|
||||||
|
|
||||||
|
collectFromLabel(piece?.constructeursLabel)
|
||||||
|
collectFromLabel(piece?.supplierLabel)
|
||||||
|
collectFromLabel(piece?.product?.constructeursLabel)
|
||||||
|
collectFromLabel(piece?.product?.supplierLabel)
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildPieceSuppliersDisplay = (piece: Record<string, any>) => {
|
||||||
|
const suppliers = resolvePieceSuppliers(piece)
|
||||||
|
const visible = suppliers.slice(0, MAX_VISIBLE_SUPPLIERS)
|
||||||
|
const overflow = Math.max(suppliers.length - visible.length, 0)
|
||||||
|
return {
|
||||||
|
suppliers,
|
||||||
|
visible,
|
||||||
|
overflow,
|
||||||
|
tooltip: suppliers.length ? suppliers.join(', ') : '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resolveDeleteGuard = (piece: Record<string, any>) => {
|
const resolveDeleteGuard = (piece: Record<string, any>) => {
|
||||||
const blockingReasons: string[] = []
|
const blockingReasons: string[] = []
|
||||||
const machineLinks = Array.isArray(piece?.machineLinks)
|
const machineLinks = Array.isArray(piece?.machineLinks)
|
||||||
@@ -269,6 +374,13 @@ const visiblePieces = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pieceRows = computed(() =>
|
||||||
|
visiblePieces.value.map((piece) => ({
|
||||||
|
piece,
|
||||||
|
suppliers: buildPieceSuppliersDisplay(piece),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
const handleDeletePiece = async (piece: Record<string, any>) => {
|
const handleDeletePiece = async (piece: Record<string, any>) => {
|
||||||
const { blockingReasons, hasCustomFields } = resolveDeleteGuard(piece)
|
const { blockingReasons, hasCustomFields } = resolveDeleteGuard(piece)
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,7 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||||
|
:initial-options="piece?.constructeurs || []"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -393,6 +394,7 @@ import { useCustomFields } from '~/composables/useCustomFields'
|
|||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
import { getFileIcon } from '~/utils/fileIcons'
|
import { getFileIcon } from '~/utils/fileIcons'
|
||||||
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
import { canPreviewDocument, isImageDocument, isPdfDocument } from '~/utils/documentPreview'
|
||||||
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
import { formatPieceStructurePreview } from '~/shared/modelUtils'
|
||||||
@@ -425,6 +427,7 @@ const { updatePiece } = usePieces()
|
|||||||
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
const { upsertCustomFieldValue, updateCustomFieldValue } = useCustomFields()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments()
|
const { loadDocumentsByPiece, uploadDocuments, deleteDocument } = useDocuments()
|
||||||
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
|
|
||||||
const piece = ref<any | null>(null)
|
const piece = ref<any | null>(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -682,6 +685,9 @@ watch(
|
|||||||
)
|
)
|
||||||
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
|
editionForm.prix = currentPiece.prix !== null && currentPiece.prix !== undefined ? String(currentPiece.prix) : ''
|
||||||
editionForm.productId = currentPiece.product?.id || currentPiece.productId || null
|
editionForm.productId = currentPiece.product?.id || currentPiece.productId || null
|
||||||
|
if (editionForm.constructeurIds.length) {
|
||||||
|
void ensureConstructeurs(editionForm.constructeurIds)
|
||||||
|
}
|
||||||
|
|
||||||
customFieldInputs.value = buildCustomFieldInputs(
|
customFieldInputs.value = buildCustomFieldInputs(
|
||||||
currentType?.structure ?? null,
|
currentType?.structure ?? null,
|
||||||
@@ -719,13 +725,15 @@ const submitEdition = async () => {
|
|||||||
? ''
|
? ''
|
||||||
: String(editionForm.prix).trim()
|
: String(editionForm.prix).trim()
|
||||||
|
|
||||||
|
const constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
|
||||||
|
|
||||||
const payload: Record<string, any> = {
|
const payload: Record<string, any> = {
|
||||||
name: editionForm.name.trim(),
|
name: editionForm.name.trim(),
|
||||||
|
constructeurIds,
|
||||||
}
|
}
|
||||||
|
|
||||||
const reference = editionForm.reference.trim()
|
const reference = editionForm.reference.trim()
|
||||||
payload.reference = reference ? reference : null
|
payload.reference = reference ? reference : null
|
||||||
payload.constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
|
|
||||||
|
|
||||||
const selectedProductId =
|
const selectedProductId =
|
||||||
typeof editionForm.productId === 'string'
|
typeof editionForm.productId === 'string'
|
||||||
|
|||||||
@@ -485,9 +485,7 @@ const submitCreation = async () => {
|
|||||||
payload.reference = reference
|
payload.reference = reference
|
||||||
}
|
}
|
||||||
|
|
||||||
if (creationForm.constructeurIds.length) {
|
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
||||||
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedProductId =
|
const selectedProductId =
|
||||||
typeof creationForm.productId === 'string'
|
typeof creationForm.productId === 'string'
|
||||||
|
|||||||
@@ -101,28 +101,44 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="product in filteredProducts" :key="product.id">
|
<tr v-for="row in productRows" :key="row.product.id">
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<DocumentThumbnail
|
<DocumentThumbnail
|
||||||
:document="resolvePrimaryDocument(product)"
|
:document="resolvePrimaryDocument(row.product)"
|
||||||
:alt="resolvePreviewAlt(product)"
|
:alt="resolvePreviewAlt(row.product)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-medium">{{ product.name }}</td>
|
<td class="font-medium">{{ row.product.name }}</td>
|
||||||
<td>{{ product.reference || '—' }}</td>
|
<td>{{ row.product.reference || '—' }}</td>
|
||||||
<td>{{ product.typeProduct?.name || '—' }}</td>
|
<td>{{ row.product.typeProduct?.name || '—' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="product.constructeurs?.length" class="text-sm">
|
<div
|
||||||
{{ formatConstructeurs(product.constructeurs) }}
|
v-if="row.suppliers.visible.length"
|
||||||
</span>
|
class="flex max-w-[14rem] flex-wrap items-center gap-1 text-sm"
|
||||||
|
:title="row.suppliers.tooltip"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="supplier in row.suppliers.visible"
|
||||||
|
:key="supplier"
|
||||||
|
class="badge badge-ghost badge-sm whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{{ supplier }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="row.suppliers.overflow"
|
||||||
|
class="badge badge-outline badge-sm"
|
||||||
|
>
|
||||||
|
+{{ row.suppliers.overflow }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<span v-else class="text-sm text-base-content/50">—</span>
|
<span v-else class="text-sm text-base-content/50">—</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{{ formatPrice(product.supplierPrice) }}
|
{{ formatPrice(row.product.supplierPrice) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right space-x-2">
|
<td class="text-right space-x-2">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="`/product/${product.id}/edit`"
|
:to="`/product/${row.product.id}/edit`"
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
>
|
>
|
||||||
Modifier
|
Modifier
|
||||||
@@ -130,7 +146,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-xs text-error"
|
class="btn btn-ghost btn-xs text-error"
|
||||||
@click="confirmDelete(product)"
|
@click="confirmDelete(row.product)"
|
||||||
>
|
>
|
||||||
Supprimer
|
Supprimer
|
||||||
</button>
|
</button>
|
||||||
@@ -233,11 +249,91 @@ const formatPrice = (value: any) => {
|
|||||||
return priceFormatter.format(number)
|
return priceFormatter.format(number)
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatConstructeurs = (constructeurs: Array<Record<string, any>>) =>
|
const MAX_VISIBLE_SUPPLIERS = 3
|
||||||
constructeurs
|
|
||||||
.map((constructeur) => constructeur?.name)
|
const resolveProductSuppliers = (product: Record<string, any>) => {
|
||||||
.filter((name): name is string => Boolean(name))
|
const names: string[] = []
|
||||||
.join(', ')
|
const seen = new Set<string>()
|
||||||
|
|
||||||
|
const pushName = (maybeName: unknown) => {
|
||||||
|
if (typeof maybeName !== 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const normalized = maybeName.trim().replace(/\s+/g, ' ')
|
||||||
|
if (!normalized.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const key = normalized.toLowerCase()
|
||||||
|
if (seen.has(key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen.add(key)
|
||||||
|
names.push(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectConstructeurs = (value: unknown): void => {
|
||||||
|
if (!value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(collectConstructeurs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
pushName(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
const record = value as Record<string, any>
|
||||||
|
pushName(record?.name ?? record?.label ?? record?.companyName ?? record?.company ?? null)
|
||||||
|
if (record?.constructeur) {
|
||||||
|
collectConstructeurs(record.constructeur)
|
||||||
|
}
|
||||||
|
if (Array.isArray(record?.constructeurs)) {
|
||||||
|
collectConstructeurs(record.constructeurs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectFromLabel = (value: unknown): void => {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value
|
||||||
|
.split(/[,;\\/•·|]+/)
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach(pushName)
|
||||||
|
}
|
||||||
|
|
||||||
|
collectConstructeurs(product?.constructeurs)
|
||||||
|
collectConstructeurs(product?.constructeur)
|
||||||
|
|
||||||
|
collectFromLabel(product?.constructeursLabel)
|
||||||
|
collectFromLabel(product?.supplierLabel)
|
||||||
|
collectFromLabel(product?.suppliers)
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildSuppliersDisplay = (product: Record<string, any>) => {
|
||||||
|
const suppliers = resolveProductSuppliers(product)
|
||||||
|
const visible = suppliers.slice(0, MAX_VISIBLE_SUPPLIERS)
|
||||||
|
const overflow = Math.max(suppliers.length - visible.length, 0)
|
||||||
|
return {
|
||||||
|
suppliers,
|
||||||
|
visible,
|
||||||
|
overflow,
|
||||||
|
tooltip: suppliers.length ? suppliers.join(', ') : '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const productRows = computed(() =>
|
||||||
|
filteredProducts.value.map((product) => ({
|
||||||
|
product,
|
||||||
|
suppliers: buildSuppliersDisplay(product),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
const resolvePrimaryDocument = (product: Record<string, any>) => {
|
const resolvePrimaryDocument = (product: Record<string, any>) => {
|
||||||
const documents = Array.isArray(product?.documents) ? product.documents : []
|
const documents = Array.isArray(product?.documents) ? product.documents : []
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
placeholder="Rechercher un ou plusieurs fournisseurs..."
|
||||||
|
:initial-options="product?.constructeurs || []"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -327,6 +328,7 @@ import { useProducts } from '~/composables/useProducts'
|
|||||||
import { useCustomFields } from '~/composables/useCustomFields'
|
import { useCustomFields } from '~/composables/useCustomFields'
|
||||||
import { useToast } from '~/composables/useToast'
|
import { useToast } from '~/composables/useToast'
|
||||||
import { useDocuments } from '~/composables/useDocuments'
|
import { useDocuments } from '~/composables/useDocuments'
|
||||||
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
|
import { formatProductStructurePreview, normalizeProductStructureForSave } from '~/shared/modelUtils'
|
||||||
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
import { uniqueConstructeurIds } from '~/shared/constructeurUtils'
|
||||||
import { getModelType } from '~/services/modelTypes'
|
import { getModelType } from '~/services/modelTypes'
|
||||||
@@ -356,6 +358,7 @@ const {
|
|||||||
uploadDocuments: uploadProductDocuments,
|
uploadDocuments: uploadProductDocuments,
|
||||||
deleteDocument: deleteProductDocument,
|
deleteDocument: deleteProductDocument,
|
||||||
} = useDocuments()
|
} = useDocuments()
|
||||||
|
const { ensureConstructeurs } = useConstructeurs()
|
||||||
|
|
||||||
const product = ref<any | null>(null)
|
const product = ref<any | null>(null)
|
||||||
const productType = ref<any | null>(null)
|
const productType = ref<any | null>(null)
|
||||||
@@ -490,7 +493,7 @@ const loadProduct = async () => {
|
|||||||
product.value = result.data
|
product.value = result.data
|
||||||
productDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
|
productDocuments.value = Array.isArray(result.data?.documents) ? result.data.documents : []
|
||||||
await loadProductType()
|
await loadProductType()
|
||||||
hydrateForm()
|
await hydrateForm()
|
||||||
await refreshDocuments()
|
await refreshDocuments()
|
||||||
} else {
|
} else {
|
||||||
product.value = null
|
product.value = null
|
||||||
@@ -566,7 +569,7 @@ const loadProductType = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydrateForm = () => {
|
const hydrateForm = async () => {
|
||||||
if (!product.value) {
|
if (!product.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -580,6 +583,9 @@ const hydrateForm = () => {
|
|||||||
? String(product.value.supplierPrice)
|
? String(product.value.supplierPrice)
|
||||||
: ''
|
: ''
|
||||||
customFieldInputs.value = buildCustomFieldInputs(structure.value, product.value.customFieldValues)
|
customFieldInputs.value = buildCustomFieldInputs(structure.value, product.value.customFieldValues)
|
||||||
|
if (editionForm.constructeurIds.length) {
|
||||||
|
await ensureConstructeurs(editionForm.constructeurIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -677,10 +683,12 @@ const submitEdition = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const constructeurIds = uniqueConstructeurIds(editionForm.constructeurIds)
|
||||||
|
|
||||||
const payload: Record<string, any> = {
|
const payload: Record<string, any> = {
|
||||||
name: editionForm.name.trim(),
|
name: editionForm.name.trim(),
|
||||||
reference: editionForm.reference.trim() || null,
|
reference: editionForm.reference.trim() || null,
|
||||||
constructeurIds: uniqueConstructeurIds(editionForm.constructeurIds),
|
constructeurIds,
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawPrice = editionForm.supplierPrice.trim()
|
const rawPrice = editionForm.supplierPrice.trim()
|
||||||
|
|||||||
@@ -423,9 +423,7 @@ const buildPayload = () => {
|
|||||||
payload.reference = reference
|
payload.reference = reference
|
||||||
}
|
}
|
||||||
|
|
||||||
if (creationForm.constructeurIds.length) {
|
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
||||||
payload.constructeurIds = uniqueConstructeurIds(creationForm.constructeurIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawPrice = creationForm.supplierPrice.trim()
|
const rawPrice = creationForm.supplierPrice.trim()
|
||||||
if (rawPrice) {
|
if (rawPrice) {
|
||||||
|
|||||||
Reference in New Issue
Block a user