Compare commits
2 Commits
a7415964a7
...
c82c21c0cd
| Author | SHA1 | Date | |
|---|---|---|---|
| c82c21c0cd | |||
| a339e722a6 |
@@ -42,6 +42,7 @@
|
|||||||
Rattachée à {{ piece.parentComponentName }}
|
Rattachée à {{ piece.parentComponentName }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="pieceData.reference" class="badge badge-outline badge-sm">{{ pieceData.reference }}</span>
|
<span v-if="pieceData.reference" class="badge badge-outline badge-sm">{{ pieceData.reference }}</span>
|
||||||
|
<span v-if="pieceData.referenceAuto" class="badge badge-secondary badge-sm" title="Référence auto">{{ pieceData.referenceAuto }}</span>
|
||||||
<template v-if="pieceConstructeursDisplay.length">
|
<template v-if="pieceConstructeursDisplay.length">
|
||||||
<span
|
<span
|
||||||
v-for="constructeur in pieceConstructeursDisplay"
|
v-for="constructeur in pieceConstructeursDisplay"
|
||||||
@@ -106,6 +107,10 @@
|
|||||||
pieceData.reference || "Non définie"
|
pieceData.reference || "Non définie"
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="pieceData.referenceAuto">
|
||||||
|
<span class="font-medium">Référence auto:</span>
|
||||||
|
<span class="ml-2">{{ pieceData.referenceAuto }}</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium">Fournisseur:</span>
|
<span class="font-medium">Fournisseur:</span>
|
||||||
<div v-if="!isEditMode" class="ml-2">
|
<div v-if="!isEditMode" class="ml-2">
|
||||||
@@ -301,6 +306,7 @@ const emit = defineEmits(['update', 'edit', 'custom-field-update', 'delete'])
|
|||||||
const pieceData = reactive({
|
const pieceData = reactive({
|
||||||
name: props.piece.name || '',
|
name: props.piece.name || '',
|
||||||
reference: props.piece.reference || '',
|
reference: props.piece.reference || '',
|
||||||
|
referenceAuto: props.piece.referenceAuto || null,
|
||||||
prix: props.piece.prix || '',
|
prix: props.piece.prix || '',
|
||||||
productId: props.piece.product?.id || props.piece.productId || null,
|
productId: props.piece.product?.id || props.piece.productId || null,
|
||||||
quantity: props.piece.quantity ?? 1,
|
quantity: props.piece.quantity ?? 1,
|
||||||
|
|||||||
@@ -108,6 +108,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<ReferenceFormulaBuilder
|
||||||
|
v-if="form.category === 'PIECE' || form.category === 'COMPONENT'"
|
||||||
|
v-model="form.referenceFormula"
|
||||||
|
:custom-fields="formulaBuilderCustomFields"
|
||||||
|
:disabled="isReadonly"
|
||||||
|
/>
|
||||||
|
|
||||||
<footer class="flex flex-col gap-3 border-t border-base-300 pt-4 sm:flex-row sm:justify-end">
|
<footer class="flex flex-col gap-3 border-t border-base-300 pt-4 sm:flex-row sm:justify-end">
|
||||||
<button type="button" class="btn btn-ghost" :disabled="saving" @click="emit('cancel')">
|
<button type="button" class="btn btn-ghost" :disabled="saving" @click="emit('cancel')">
|
||||||
Annuler
|
Annuler
|
||||||
@@ -177,14 +184,33 @@ const componentSubcomponentMaxDepth = computed(() =>
|
|||||||
)
|
)
|
||||||
const isReadonly = computed(() => props.readonly === true)
|
const isReadonly = computed(() => props.readonly === true)
|
||||||
|
|
||||||
const form = reactive<ModelTypePayload>({
|
const form = reactive<ModelTypePayload & { referenceFormula?: string | null }>({
|
||||||
name: '',
|
name: '',
|
||||||
code: '',
|
code: '',
|
||||||
category: props.initialCategory,
|
category: props.initialCategory,
|
||||||
notes: '',
|
notes: '',
|
||||||
structure: undefined,
|
structure: undefined,
|
||||||
|
referenceFormula: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const formulaBuilderCustomFields = computed(() => {
|
||||||
|
if (form.category === 'PIECE') {
|
||||||
|
const fields = pieceStructure.value?.customFields
|
||||||
|
return Array.isArray(fields) ? fields : []
|
||||||
|
}
|
||||||
|
if (form.category === 'COMPONENT') {
|
||||||
|
const fields = componentStructure.value?.customFields
|
||||||
|
return Array.isArray(fields) ? fields : []
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
const extractFormulaFields = (formula: string | null | undefined): string[] => {
|
||||||
|
if (!formula) return []
|
||||||
|
const matches = [...formula.matchAll(/\{(\w+)\}/g)]
|
||||||
|
return [...new Set(matches.map(m => m[1]).filter((n): n is string => n !== undefined))]
|
||||||
|
}
|
||||||
|
|
||||||
const errors = reactive<{ name?: string }>({})
|
const errors = reactive<{ name?: string }>({})
|
||||||
const nameInput = ref<HTMLInputElement | null>(null)
|
const nameInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
@@ -248,6 +274,9 @@ const resetForm = () => {
|
|||||||
|
|
||||||
errors.name = undefined
|
errors.name = undefined
|
||||||
|
|
||||||
|
const incomingAny = incoming as Record<string, unknown>
|
||||||
|
form.referenceFormula = typeof incomingAny.referenceFormula === 'string' ? incomingAny.referenceFormula : null
|
||||||
|
|
||||||
resetStructures(incoming.structure, form.category)
|
resetStructures(incoming.structure, form.category)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,20 +315,28 @@ const handleSubmit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (form.category === 'COMPONENT') {
|
if (form.category === 'COMPONENT') {
|
||||||
|
const formula = form.referenceFormula || null
|
||||||
|
const requiredFields = extractFormulaFields(formula)
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
...common,
|
...common,
|
||||||
category: 'COMPONENT',
|
category: 'COMPONENT',
|
||||||
structure: normalizeStructureForSave(cloneStructure(componentStructure.value)),
|
structure: normalizeStructureForSave(cloneStructure(componentStructure.value)),
|
||||||
})
|
referenceFormula: formula,
|
||||||
|
requiredFieldsForReference: requiredFields.length ? requiredFields : null,
|
||||||
|
} as ModelTypePayload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.category === 'PIECE') {
|
if (form.category === 'PIECE') {
|
||||||
|
const formula = form.referenceFormula || null
|
||||||
|
const requiredFields = extractFormulaFields(formula)
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
...common,
|
...common,
|
||||||
category: 'PIECE',
|
category: 'PIECE',
|
||||||
structure: normalizePieceStructureForSave(clonePieceStructure(pieceStructure.value)),
|
structure: normalizePieceStructureForSave(clonePieceStructure(pieceStructure.value)),
|
||||||
})
|
referenceFormula: formula,
|
||||||
|
requiredFieldsForReference: requiredFields.length ? requiredFields : null,
|
||||||
|
} as ModelTypePayload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<section class="space-y-4">
|
||||||
|
<header>
|
||||||
|
<h3 class="text-lg font-semibold text-base-content">Génération de référence automatique</h3>
|
||||||
|
<p class="mt-1 text-sm text-base-content/70">
|
||||||
|
Cliquez sur un champ pour l'insérer dans la formule. Vous pouvez aussi taper du texte libre (séparateurs, préfixes…).
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-base-300 p-4 space-y-4">
|
||||||
|
<div v-if="fieldNames.length" class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
v-for="name in fieldNames"
|
||||||
|
:key="name"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-xs btn-outline btn-primary font-mono"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="insertField(name)"
|
||||||
|
>
|
||||||
|
{{ name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-sm text-base-content/50 italic">
|
||||||
|
Aucun champ personnalisé défini dans la structure.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="label" for="reference-formula">
|
||||||
|
<span class="label-text">Formule</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="reference-formula"
|
||||||
|
ref="inputRef"
|
||||||
|
:value="modelValue"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full font-mono"
|
||||||
|
placeholder="Ex: SNU {serie}-{diametre}/{type}"
|
||||||
|
:disabled="disabled"
|
||||||
|
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value || null)"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-base-content/60">
|
||||||
|
Laissez vide si ce type n'utilise pas de référence automatique.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="modelValue" class="rounded bg-base-200 px-3 py-2 text-sm">
|
||||||
|
<span class="text-base-content/70">Aperçu :</span>
|
||||||
|
<span class="ml-1 font-mono font-semibold">{{ preview }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, ref } from 'vue'
|
||||||
|
|
||||||
|
interface CustomField {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: string | null | undefined
|
||||||
|
customFields: CustomField[]
|
||||||
|
disabled?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string | null): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
const fieldNames = computed(() =>
|
||||||
|
props.customFields.map(f => f.name).filter((n): n is string => Boolean(n)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const previewExamples: Record<string, string> = {
|
||||||
|
text: 'VALEUR',
|
||||||
|
number: '123',
|
||||||
|
select: 'OPTION',
|
||||||
|
boolean: 'OUI',
|
||||||
|
date: '2026-01-01',
|
||||||
|
}
|
||||||
|
|
||||||
|
const preview = computed(() => {
|
||||||
|
if (!props.modelValue) return ''
|
||||||
|
const fieldMap = new Map<string, string>()
|
||||||
|
for (const f of props.customFields) {
|
||||||
|
if (f.name) {
|
||||||
|
fieldMap.set(f.name, previewExamples[f.type] ?? 'VALEUR')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props.modelValue.replace(/\{(\w+)\}/g, (_, name) => fieldMap.get(name) ?? '???')
|
||||||
|
})
|
||||||
|
|
||||||
|
const insertField = (fieldName: string) => {
|
||||||
|
const placeholder = `{${fieldName}}`
|
||||||
|
const input = inputRef.value
|
||||||
|
const current = props.modelValue ?? ''
|
||||||
|
if (!input) {
|
||||||
|
emit('update:modelValue', current + placeholder)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const start = input.selectionStart ?? current.length
|
||||||
|
const end = input.selectionEnd ?? start
|
||||||
|
const updated = current.slice(0, start) + placeholder + current.slice(end)
|
||||||
|
emit('update:modelValue', updated)
|
||||||
|
nextTick(() => {
|
||||||
|
const newPos = start + placeholder.length
|
||||||
|
input.focus()
|
||||||
|
input.setSelectionRange(newPos, newPos)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -10,6 +10,7 @@ export interface Piece {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
reference?: string | null
|
reference?: string | null
|
||||||
|
referenceAuto?: string | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
typePieceId?: string | null
|
typePieceId?: string | null
|
||||||
typePiece?: { id: string; name?: string } | null
|
typePiece?: { id: string; name?: string } | null
|
||||||
|
|||||||
@@ -72,6 +72,28 @@ const badgeClass = (type: ChangeType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const releases: Release[] = [
|
const releases: Release[] = [
|
||||||
|
{
|
||||||
|
version: "v1.9.5",
|
||||||
|
date: "2026-03-31",
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
text: "Référence automatique des pièces et composants : génération d'une référence technique à partir d'une formule configurable sur la catégorie, recalculée automatiquement à chaque modification des champs personnalisés",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
text: "Formula builder interactif : sélection des champs disponibles par clic (chips) avec insertion à la position du curseur, aperçu live avec valeurs d'exemple, et calcul automatique des champs requis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
text: "Versioning des entités : numéro de version incrémenté automatiquement à chaque modification, avec historique des versions et possibilité de restaurer une version antérieure",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "feat",
|
||||||
|
text: "Bouton de sauvegarde unique sur la fiche machine : remplacement des sauvegardes automatiques par un bouton explicite, avec affichage des versions sur les liens composants/pièces/produits",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: "v1.9.4",
|
version: "v1.9.4",
|
||||||
date: "2026-03-25",
|
date: "2026-03-25",
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ const loadCategory = async () => {
|
|||||||
category: response.category,
|
category: response.category,
|
||||||
notes: response.notes ?? response.description ?? '',
|
notes: response.notes ?? response.description ?? '',
|
||||||
structure: (response.structure as ComponentModelStructure | null) ?? undefined,
|
structure: (response.structure as ComponentModelStructure | null) ?? undefined,
|
||||||
|
referenceFormula: response.referenceFormula ?? null,
|
||||||
|
requiredFieldsForReference: response.requiredFieldsForReference ?? null,
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -113,6 +113,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Référence auto (read-only, shown only if computed) -->
|
||||||
|
<div v-if="piece.referenceAuto" class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Référence auto</span>
|
||||||
|
</label>
|
||||||
|
<div class="input input-bordered input-sm md:input-md bg-base-200 flex items-center gap-2">
|
||||||
|
<span class="font-mono font-semibold">{{ piece.referenceAuto }}</span>
|
||||||
|
<span class="badge badge-sm badge-ghost">auto</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Référence + Fournisseurs (if value or edit mode) -->
|
<!-- Référence + Fournisseurs (if value or edit mode) -->
|
||||||
<div
|
<div
|
||||||
v-if="isEditMode || piece.reference || editionForm.constructeurIds.length"
|
v-if="isEditMode || piece.reference || editionForm.constructeurIds.length"
|
||||||
|
|||||||
@@ -69,6 +69,10 @@
|
|||||||
{{ row.piece.reference || '—' }}
|
{{ row.piece.reference || '—' }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #cell-referenceAuto="{ row }">
|
||||||
|
{{ row.piece.referenceAuto || '—' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #cell-description="{ row }">
|
<template #cell-description="{ row }">
|
||||||
<div v-if="row.piece.description" class="group relative">
|
<div v-if="row.piece.description" class="group relative">
|
||||||
<span class="block cursor-help truncate">{{ row.piece.description }}</span>
|
<span class="block cursor-help truncate">{{ row.piece.description }}</span>
|
||||||
@@ -174,6 +178,7 @@ const columns = [
|
|||||||
{ key: 'preview', label: 'Aperçu', width: 'w-24' },
|
{ key: 'preview', label: 'Aperçu', width: 'w-24' },
|
||||||
{ key: 'name', label: 'Nom', sortable: true },
|
{ key: 'name', label: 'Nom', sortable: true },
|
||||||
{ key: 'reference', label: 'Référence' },
|
{ key: 'reference', label: 'Référence' },
|
||||||
|
{ key: 'referenceAuto', label: 'Réf. auto' },
|
||||||
{ key: 'description', label: 'Description' },
|
{ key: 'description', label: 'Description' },
|
||||||
{ key: 'suppliers', label: 'Fournisseurs' },
|
{ key: 'suppliers', label: 'Fournisseurs' },
|
||||||
{ key: 'typePiece', label: 'Type de pièce', filterable: true, filterPlaceholder: 'Filtrer…' },
|
{ key: 'typePiece', label: 'Type de pièce', filterable: true, filterPlaceholder: 'Filtrer…' },
|
||||||
|
|||||||
@@ -114,6 +114,19 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="piece?.referenceAuto" class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Référence auto</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:value="piece.referenceAuto"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm md:input-md bg-base-200"
|
||||||
|
disabled
|
||||||
|
title="Générée automatiquement à partir du type et des champs personnalisés"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Fournisseur</span>
|
<span class="label-text">Fournisseur</span>
|
||||||
|
|||||||
@@ -23,11 +23,15 @@ export interface BaseModelTypePayload {
|
|||||||
export interface ComponentModelTypePayload extends BaseModelTypePayload {
|
export interface ComponentModelTypePayload extends BaseModelTypePayload {
|
||||||
category: 'COMPONENT';
|
category: 'COMPONENT';
|
||||||
structure?: ComponentModelStructure | null;
|
structure?: ComponentModelStructure | null;
|
||||||
|
referenceFormula?: string | null;
|
||||||
|
requiredFieldsForReference?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PieceModelTypePayload extends BaseModelTypePayload {
|
export interface PieceModelTypePayload extends BaseModelTypePayload {
|
||||||
category: 'PIECE';
|
category: 'PIECE';
|
||||||
structure?: PieceModelStructure | null;
|
structure?: PieceModelStructure | null;
|
||||||
|
referenceFormula?: string | null;
|
||||||
|
requiredFieldsForReference?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductModelTypePayload extends BaseModelTypePayload {
|
export interface ProductModelTypePayload extends BaseModelTypePayload {
|
||||||
@@ -46,6 +50,8 @@ export interface ModelType extends BaseModelTypePayload {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
category: ModelCategory;
|
category: ModelCategory;
|
||||||
structure: ModelTypeStructure;
|
structure: ModelTypeStructure;
|
||||||
|
referenceFormula?: string | null;
|
||||||
|
requiredFieldsForReference?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelTypeListParams {
|
export interface ModelTypeListParams {
|
||||||
|
|||||||
Reference in New Issue
Block a user