refactor(machines) : remove TypeMachine skeleton system, simplify machine creation
- Remove TypeEdit*, TypeInfoDisplay, MachineSkeletonSummary, MachineCreatePreview components - Remove machine-skeleton pages and type pages - Remove useMachineTypesApi, useMachineSkeletonEditor, useMachineCreateSelections composables - Add AddEntityToMachineModal for direct entity linking - Update machine detail/create pages for direct custom fields - Fix SearchSelect, category display, and ipartial search filters Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -887,7 +887,6 @@ const submitEdition = async () => {
|
||||
updatedComponent.id,
|
||||
[
|
||||
updatedComponent?.typeComposant?.customFields,
|
||||
updatedComponent?.typeMachineComponentRequirement?.typeComposant?.customFields,
|
||||
],
|
||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||
)
|
||||
|
||||
@@ -1219,7 +1219,6 @@ const saveCustomFieldValues = async (createdComponent: any) => {
|
||||
}
|
||||
|
||||
registerDefinitions(createdComponent?.typeComposant?.customFields)
|
||||
registerDefinitions(createdComponent?.typeMachineComponentRequirement?.typeComposant?.customFields)
|
||||
|
||||
const resolveDefinitionId = (field: CustomFieldInput) => {
|
||||
if (field.customFieldId) {
|
||||
|
||||
@@ -49,35 +49,18 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Type de machine</span>
|
||||
<span class="label-text">Site</span>
|
||||
</label>
|
||||
<select v-model="selectedType" class="select select-bordered">
|
||||
<select v-model="selectedSiteFilter" class="select select-bordered">
|
||||
<option value="">
|
||||
Tous les types
|
||||
Tous les sites
|
||||
</option>
|
||||
<option
|
||||
v-for="type in machineTypes"
|
||||
:key="type.id"
|
||||
:value="type.id"
|
||||
v-for="site in sites"
|
||||
:key="site.id"
|
||||
:value="site.id"
|
||||
>
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Catégorie</span>
|
||||
</label>
|
||||
<select v-model="selectedCategory" class="select select-bordered">
|
||||
<option value="">
|
||||
Toutes les catégories
|
||||
</option>
|
||||
<option
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
:value="category"
|
||||
>
|
||||
{{ category }}
|
||||
{{ site.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -208,27 +191,9 @@
|
||||
<h4 class="font-semibold text-sm">
|
||||
{{ machine.name }}
|
||||
</h4>
|
||||
<div
|
||||
class="badge badge-sm"
|
||||
:class="
|
||||
getCategoryBadgeClass(machine.typeMachine?.category)
|
||||
"
|
||||
>
|
||||
{{ machine.typeMachine?.category || "N/A" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 text-xs text-gray-600">
|
||||
<div class="flex items-center gap-1">
|
||||
<IconLucideSettings2
|
||||
class="w-3 h-3"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{
|
||||
machine.typeMachine?.name || "Type inconnu"
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="machine.reference"
|
||||
class="flex items-center gap-1"
|
||||
@@ -373,78 +338,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Type de machine</span>
|
||||
</label>
|
||||
<select
|
||||
v-model="newMachine.typeMachineId"
|
||||
class="select select-bordered"
|
||||
:disabled="!canEdit"
|
||||
required
|
||||
>
|
||||
<option value="">
|
||||
Sélectionner un type
|
||||
</option>
|
||||
<option
|
||||
v-for="type in machineTypes"
|
||||
:key="type.id"
|
||||
:value="type.id"
|
||||
>
|
||||
{{ type.name }} ({{ type.category }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Référence</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="newMachine.reference"
|
||||
type="text"
|
||||
placeholder="Ex: PRESS-001"
|
||||
class="input input-bordered"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type Preview -->
|
||||
<div
|
||||
v-if="selectedMachineType"
|
||||
class="mb-4 p-4 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<h4 class="font-semibold text-sm mb-2">
|
||||
Structure du type sélectionné :
|
||||
</h4>
|
||||
<div class="text-xs space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">Familles de composants :</span>
|
||||
<span class="badge badge-sm">{{
|
||||
selectedMachineType.componentRequirements?.length || 0
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">Groupes de pièces :</span>
|
||||
<span class="badge badge-sm">{{
|
||||
selectedMachineType.pieceRequirements?.length || 0
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">Produits requis :</span>
|
||||
<span class="badge badge-sm">{{
|
||||
selectedMachineType.productRequirements?.length || 0
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">Catégorie :</span>
|
||||
<span class="badge badge-outline badge-sm">{{
|
||||
selectedMachineType.category
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Référence</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="newMachine.reference"
|
||||
type="text"
|
||||
placeholder="Ex: PRESS-001"
|
||||
class="input input-bordered"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
@@ -469,7 +373,6 @@
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import SiteContactFormFields from '~/components/sites/SiteContactFormFields.vue'
|
||||
import { useSites } from '~/composables/useSites'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useMachines } from '~/composables/useMachines'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { humanizeError } from '~/shared/utils/errorMessages'
|
||||
@@ -479,22 +382,19 @@ import IconLucideUser from '~icons/lucide/user'
|
||||
import IconLucidePhone from '~icons/lucide/phone'
|
||||
import IconLucideMapPinned from '~icons/lucide/map-pinned'
|
||||
import IconLucideChevronDown from '~icons/lucide/chevron-down'
|
||||
import IconLucideSettings2 from '~icons/lucide/settings-2'
|
||||
import IconLucideTag from '~icons/lucide/tag'
|
||||
import { formatPhone } from '~/utils/formatters/phone'
|
||||
import { extractRelationId } from '~/shared/apiRelations'
|
||||
|
||||
const { canEdit } = usePermissions()
|
||||
const { sites, loading, loadSites, createSite } = useSites()
|
||||
const { machineTypes, loadMachineTypes } = useMachineTypesApi()
|
||||
const { machines, loadMachines, createMachineFromType, deleteMachine } = useMachines()
|
||||
const { machines, loadMachines, createMachine, deleteMachine } = useMachines()
|
||||
|
||||
// Data
|
||||
const showAddSiteModal = ref(false)
|
||||
const showAddMachineModal = ref(false)
|
||||
const searchTerm = ref('')
|
||||
const selectedType = ref('')
|
||||
const selectedCategory = ref('')
|
||||
const selectedSiteFilter = ref('')
|
||||
const collapsedSites = ref([])
|
||||
|
||||
const newSite = reactive({
|
||||
@@ -509,48 +409,14 @@ const newSite = reactive({
|
||||
const newMachine = reactive({
|
||||
name: '',
|
||||
siteId: '',
|
||||
typeMachineId: '',
|
||||
reference: ''
|
||||
})
|
||||
|
||||
// Computed
|
||||
const selectedMachineType = computed(() => {
|
||||
if (!newMachine.typeMachineId) { return null }
|
||||
return machineTypes.value.find(
|
||||
type => type.id === newMachine.typeMachineId
|
||||
)
|
||||
})
|
||||
|
||||
const categories = computed(() => {
|
||||
const cats = new Set()
|
||||
machineTypes.value.forEach((type) => {
|
||||
if (type.category) { cats.add(type.category) }
|
||||
})
|
||||
return Array.from(cats)
|
||||
})
|
||||
|
||||
const machinesWithType = computed(() => {
|
||||
return machines.value.map((machine) => {
|
||||
const resolvedTypeMachineId = machine.typeMachineId || extractRelationId(machine.typeMachine)
|
||||
const resolvedTypeMachine = resolvedTypeMachineId
|
||||
? machineTypes.value.find(type => type.id === resolvedTypeMachineId) || null
|
||||
: null
|
||||
|
||||
return {
|
||||
...machine,
|
||||
typeMachineId: resolvedTypeMachineId || machine.typeMachineId,
|
||||
typeMachine:
|
||||
machine.typeMachine && typeof machine.typeMachine === 'object'
|
||||
? machine.typeMachine
|
||||
: resolvedTypeMachine
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const machinesBySiteId = computed(() => {
|
||||
const map = new Map()
|
||||
|
||||
machinesWithType.value.forEach((machine) => {
|
||||
machines.value.forEach((machine) => {
|
||||
const siteId = machine.siteId || extractRelationId(machine.site)
|
||||
if (!siteId) { return }
|
||||
|
||||
@@ -588,6 +454,11 @@ const formatPhoneDisplay = (value) => {
|
||||
const filteredSites = computed(() => {
|
||||
let filtered = sitesWithMachines.value
|
||||
|
||||
// Filtrer par site
|
||||
if (selectedSiteFilter.value) {
|
||||
filtered = filtered.filter(site => site.id === selectedSiteFilter.value)
|
||||
}
|
||||
|
||||
// Filtrer par terme de recherche
|
||||
if (searchTerm.value) {
|
||||
filtered = filtered.filter((site) => {
|
||||
@@ -616,33 +487,6 @@ const filteredSites = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// Filtrer par type de machine
|
||||
if (selectedType.value) {
|
||||
filtered = filtered
|
||||
.map(site => ({
|
||||
...site,
|
||||
machines:
|
||||
site.machines?.filter(
|
||||
machine => machine.typeMachineId === selectedType.value
|
||||
) || []
|
||||
}))
|
||||
.filter(site => site.machines.length > 0)
|
||||
}
|
||||
|
||||
// Filtrer par catégorie
|
||||
if (selectedCategory.value) {
|
||||
filtered = filtered
|
||||
.map(site => ({
|
||||
...site,
|
||||
machines:
|
||||
site.machines?.filter(
|
||||
machine =>
|
||||
machine.typeMachine?.category === selectedCategory.value
|
||||
) || []
|
||||
}))
|
||||
.filter(site => site.machines.length > 0)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
@@ -670,27 +514,15 @@ const handleCreateSite = async () => {
|
||||
}
|
||||
|
||||
const handleCreateMachine = async () => {
|
||||
if (!selectedMachineType.value) {
|
||||
console.error('Aucun type de machine sélectionné')
|
||||
return
|
||||
}
|
||||
|
||||
const machineData = {
|
||||
const result = await createMachine({
|
||||
name: newMachine.name,
|
||||
siteId: newMachine.siteId,
|
||||
reference: newMachine.reference
|
||||
}
|
||||
|
||||
const result = await createMachineFromType(
|
||||
machineData,
|
||||
selectedMachineType.value
|
||||
)
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
// Reset form
|
||||
newMachine.name = ''
|
||||
newMachine.siteId = ''
|
||||
newMachine.typeMachineId = ''
|
||||
newMachine.reference = ''
|
||||
showAddMachineModal.value = false
|
||||
await loadMachines()
|
||||
@@ -745,19 +577,8 @@ const addMachineToSite = (site) => {
|
||||
showAddMachineModal.value = true
|
||||
}
|
||||
|
||||
const getCategoryBadgeClass = (category) => {
|
||||
const classes = {
|
||||
Production: 'badge-primary',
|
||||
Transformation: 'badge-secondary',
|
||||
Manutention: 'badge-accent',
|
||||
Traitement: 'badge-info',
|
||||
Contrôle: 'badge-warning'
|
||||
}
|
||||
return classes[category] || 'badge-neutral'
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadSites(), loadMachineTypes(), loadMachines()])
|
||||
await Promise.all([loadSites(), loadMachines()])
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<!-- Machine Types List -->
|
||||
<div class="my-8">
|
||||
<!-- Header with Add Button -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-800">Squelettes de machine</h2>
|
||||
<NuxtLink to="/machine-skeleton/new" class="btn btn-primary">
|
||||
<IconLucidePlus class="w-5 h-5 mr-2" aria-hidden="true" />
|
||||
Créer un type
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Categories Tabs -->
|
||||
<div class="tabs tabs-boxed mb-6">
|
||||
<a
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': selectedCategory === category }"
|
||||
@click="selectedCategory = category"
|
||||
>
|
||||
{{ category }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Machine Types Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="type in filteredTypes"
|
||||
:key="type.id"
|
||||
class="card bg-base-100 shadow-lg hover:shadow-xl transition-all duration-300 cursor-pointer"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="card-title text-lg">
|
||||
{{ type.name }}
|
||||
</h3>
|
||||
<div class="badge badge-primary">
|
||||
{{ type.category }}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-600 mb-4">
|
||||
{{ type.description }}
|
||||
</p>
|
||||
<div class="space-y-2 text-sm text-gray-500">
|
||||
<div class="flex items-center gap-2">
|
||||
<IconLucidePackage class="w-4 h-4" aria-hidden="true" />
|
||||
<span
|
||||
>{{ type.componentRequirements?.length || 0 }} famille(s) de
|
||||
composants</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconLucideLayoutGrid class="w-4 h-4" aria-hidden="true" />
|
||||
<span
|
||||
>{{ type.pieceRequirements?.length || 0 }} groupe(s) de
|
||||
pièces</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconLucideBox class="w-4 h-4" aria-hidden="true" />
|
||||
<span
|
||||
>{{ type.productRequirements?.length || 0 }} produit(s)
|
||||
requis</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button
|
||||
v-if="canEdit"
|
||||
class="btn btn-sm btn-error"
|
||||
@click.stop="confirmDeleteType(type)"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
<NuxtLink :to="`/type/${type.id}`" class="btn btn-sm btn-outline">
|
||||
Voir détails
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="filteredTypes.length === 0" class="text-center py-12">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-16">
|
||||
<IconLucideLayoutGrid class="w-8 h-8" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-600 mt-4">
|
||||
Aucun type trouvé
|
||||
</h3>
|
||||
<p class="text-gray-500">
|
||||
Aucun type de machine ne correspond à cette catégorie.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useMachineTypesApi } from "~/composables/useMachineTypesApi";
|
||||
import { useToast } from "~/composables/useToast";
|
||||
import { humanizeError } from "~/shared/utils/errorMessages";
|
||||
import IconLucidePlus from "~icons/lucide/plus";
|
||||
import IconLucidePackage from "~icons/lucide/package";
|
||||
import IconLucideLayoutGrid from "~icons/lucide/layout-grid";
|
||||
import IconLucideBox from "~icons/lucide/box";
|
||||
|
||||
const { canEdit } = usePermissions();
|
||||
const { machineTypes, loadMachineTypes, deleteMachineType } =
|
||||
useMachineTypesApi();
|
||||
|
||||
const categories = ref([
|
||||
"Toutes",
|
||||
"Production",
|
||||
"Transformation",
|
||||
"Manutention",
|
||||
"Traitement",
|
||||
"Contrôle",
|
||||
]);
|
||||
|
||||
const selectedCategory = ref("Toutes");
|
||||
|
||||
const filteredTypes = computed(() => {
|
||||
if (selectedCategory.value === "Toutes") {
|
||||
return machineTypes.value;
|
||||
}
|
||||
return machineTypes.value.filter(
|
||||
(type) => type.category === selectedCategory.value
|
||||
);
|
||||
});
|
||||
|
||||
const { confirm: confirmDialog } = useConfirm();
|
||||
|
||||
const confirmDeleteType = async (type) => {
|
||||
const { showError, showSuccess } = useToast();
|
||||
|
||||
if (
|
||||
await confirmDialog({
|
||||
message: `Êtes-vous sûr de vouloir supprimer le type "${type.name}" ? Cette action est irréversible.`,
|
||||
})
|
||||
) {
|
||||
try {
|
||||
const result = await deleteMachineType(type.id);
|
||||
if (result.success) {
|
||||
showSuccess(`Type "${type.name}" supprimé avec succès`);
|
||||
} else {
|
||||
showError(`Impossible de supprimer le type : ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(`Impossible de supprimer le type : ${humanizeError(error.message)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Load machine types on mount
|
||||
onMounted(async () => {
|
||||
await loadMachineTypes();
|
||||
});
|
||||
</script>
|
||||
@@ -1,265 +0,0 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8 space-y-8">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body space-y-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<IconLucidePlus
|
||||
class="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<div>
|
||||
<h2 class="card-title text-2xl">
|
||||
Nouveau type de machine
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500">
|
||||
Complétez les informations puis enregistrez pour générer le nouveau type.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="{ 'pointer-events-none opacity-60': !canEdit }">
|
||||
<TypeEditForm
|
||||
:key="formKey"
|
||||
v-model="draftType"
|
||||
:saving="!canEdit || creating"
|
||||
:resettable="false"
|
||||
submit-label="Créer le type"
|
||||
submit-loading-label="Création..."
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="space-y-4">
|
||||
<div v-if="initialLoading" class="text-center py-12 text-sm text-gray-500">
|
||||
Chargement des types existants...
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="recentTypes.length" class="space-y-4">
|
||||
<h3 class="text-xl font-semibold">
|
||||
Types générés récemment
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<article
|
||||
v-for="type in recentTypes"
|
||||
:key="type.id"
|
||||
class="card bg-base-100 shadow-md border border-base-200"
|
||||
>
|
||||
<div class="card-body space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="card-title text-base">
|
||||
{{ type.name }}
|
||||
</h4>
|
||||
<span v-if="type.category" class="badge badge-outline badge-sm">{{ type.category }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 line-clamp-3">
|
||||
{{ type.description || 'Aucune description' }}
|
||||
</p>
|
||||
<div class="text-xs text-gray-500 flex items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<IconLucideClipboardList class="h-4 w-4" aria-hidden="true" />
|
||||
{{ type.componentRequirements?.length || 0 }} famille(s)
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<IconLucideList class="h-4 w-4" aria-hidden="true" />
|
||||
{{ type.pieceRequirements?.length || 0 }} groupe(s) de pièces
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<IconLucideBox class="h-4 w-4" aria-hidden="true" />
|
||||
{{ type.productRequirements?.length || 0 }} produit(s)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-12 text-sm text-gray-500">
|
||||
Aucun type généré récemment.
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { extractRelationId } from '~/shared/apiRelations'
|
||||
import IconLucidePlus from '~icons/lucide/plus'
|
||||
import IconLucideClipboardList from '~icons/lucide/clipboard-list'
|
||||
import IconLucideList from '~icons/lucide/list'
|
||||
import IconLucideBox from '~icons/lucide/box'
|
||||
|
||||
const { machineTypes, loadMachineTypes, createMachineType } = useMachineTypesApi()
|
||||
const { showError } = useToast()
|
||||
const { canEdit } = usePermissions()
|
||||
|
||||
const formKey = ref(0)
|
||||
const creating = ref(false)
|
||||
const initialLoading = ref(true)
|
||||
|
||||
const createEmptyType = () => ({
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
maintenanceFrequency: '',
|
||||
customFields: [],
|
||||
componentRequirements: [],
|
||||
pieceRequirements: [],
|
||||
productRequirements: []
|
||||
})
|
||||
|
||||
const draftType = ref(createEmptyType())
|
||||
|
||||
const recentTypes = computed(() => machineTypes.value.slice(-3).reverse())
|
||||
|
||||
onMounted(async () => {
|
||||
if (!machineTypes.value.length) {
|
||||
try {
|
||||
initialLoading.value = true
|
||||
await loadMachineTypes()
|
||||
} finally {
|
||||
initialLoading.value = false
|
||||
}
|
||||
} else {
|
||||
initialLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const parseOptions = (field = {}) => {
|
||||
if (field.type !== 'select') { return [] }
|
||||
if (field.optionsText && typeof field.optionsText === 'string') {
|
||||
return field.optionsText
|
||||
.split('\n')
|
||||
.map(option => option.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
if (Array.isArray(field.options)) {
|
||||
return field.options
|
||||
.map(option => String(option).trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const toModelTypeIri = (value) => {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
if (typeof value === 'string' && value.startsWith('/api/model_types/')) {
|
||||
return value
|
||||
}
|
||||
const relationId = extractRelationId(value)
|
||||
if (relationId) {
|
||||
return `/api/model_types/${relationId}`
|
||||
}
|
||||
return typeof value === 'string' ? `/api/model_types/${value}` : undefined
|
||||
}
|
||||
|
||||
const normalizeCustomFields = (fields = []) =>
|
||||
fields
|
||||
.filter(field => field?.name && field.name.trim() !== '')
|
||||
.map((field, index) => ({
|
||||
name: field.name,
|
||||
type: field.type || '',
|
||||
required: !!field.required,
|
||||
options: parseOptions(field),
|
||||
orderIndex: typeof field.orderIndex === 'number' ? field.orderIndex : index
|
||||
}))
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
.map((field, index) => ({ ...field, orderIndex: index }))
|
||||
|
||||
const toIntegerOrNull = (value, fallback = null) => {
|
||||
if (value === '' || value === undefined || value === null) {
|
||||
return fallback
|
||||
}
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
const normalizeComponentRequirements = (requirements = []) =>
|
||||
requirements
|
||||
.filter(req => req?.typeComposantId || req?.typeComposant)
|
||||
.map((req, index) => ({
|
||||
typeComposant: toModelTypeIri(req.typeComposantId || req.typeComposant),
|
||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||
minCount: toIntegerOrNull(req.minCount, 1),
|
||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||
required: req.required ?? true,
|
||||
allowNewModels: req.allowNewModels ?? true,
|
||||
orderIndex: typeof req.orderIndex === 'number' ? req.orderIndex : index
|
||||
}))
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
.map((req, index) => ({ ...req, orderIndex: index }))
|
||||
|
||||
const normalizePieceRequirements = (requirements = []) =>
|
||||
requirements
|
||||
.filter(req => req?.typePieceId || req?.typePiece)
|
||||
.map((req, index) => ({
|
||||
typePiece: toModelTypeIri(req.typePieceId || req.typePiece),
|
||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||
minCount: toIntegerOrNull(req.minCount, 0),
|
||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||
required: req.required ?? false,
|
||||
allowNewModels: req.allowNewModels ?? true,
|
||||
orderIndex: typeof req.orderIndex === 'number' ? req.orderIndex : index
|
||||
}))
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
.map((req, index) => ({ ...req, orderIndex: index }))
|
||||
|
||||
const normalizeProductRequirements = (requirements = []) =>
|
||||
requirements
|
||||
.filter(req => req?.typeProductId || req?.typeProduct)
|
||||
.map((req, index) => ({
|
||||
typeProduct: toModelTypeIri(req.typeProductId || req.typeProduct),
|
||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||
minCount: toIntegerOrNull(req.minCount, 0),
|
||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||
required: req.required ?? false,
|
||||
allowNewModels: req.allowNewModels ?? true,
|
||||
orderIndex: typeof req.orderIndex === 'number' ? req.orderIndex : index
|
||||
}))
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
.map((req, index) => ({ ...req, orderIndex: index }))
|
||||
|
||||
const buildPayload = typeData => ({
|
||||
name: typeData.name,
|
||||
description: typeData.description,
|
||||
category: typeData.category,
|
||||
maintenanceFrequency: typeData.maintenanceFrequency,
|
||||
customFields: normalizeCustomFields(typeData.customFields),
|
||||
componentRequirements: normalizeComponentRequirements(typeData.componentRequirements),
|
||||
pieceRequirements: normalizePieceRequirements(typeData.pieceRequirements),
|
||||
productRequirements: normalizeProductRequirements(typeData.productRequirements)
|
||||
})
|
||||
|
||||
const resetForm = () => {
|
||||
draftType.value = createEmptyType()
|
||||
formKey.value += 1
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!draftType.value.name?.trim()) {
|
||||
showError('Le nom du type est requis.')
|
||||
return
|
||||
}
|
||||
|
||||
const payload = buildPayload(draftType.value)
|
||||
|
||||
creating.value = true
|
||||
const result = await createMachineType(payload)
|
||||
creating.value = false
|
||||
|
||||
if (result?.success) {
|
||||
resetForm()
|
||||
} else if (result?.error) {
|
||||
showError(result.error)
|
||||
} else {
|
||||
showError('Impossible de créer le type.')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -17,117 +17,120 @@
|
||||
<!-- Header with actions -->
|
||||
<MachineDetailHeader
|
||||
:title="machineViewTitle"
|
||||
:is-details-view="s.isDetailsView.value"
|
||||
:is-skeleton-view="s.isSkeletonView.value"
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
:has-skeleton-requirements="d.machineHasSkeletonRequirements.value"
|
||||
@change-view="s.changeMachineView"
|
||||
@toggle-edit="d.toggleEditMode"
|
||||
@open-print="d.openPrintModal"
|
||||
/>
|
||||
|
||||
<template v-if="s.isDetailsView.value">
|
||||
<!-- Debug info -->
|
||||
<div v-if="d.debug.value" class="bg-yellow-100 p-4 rounded-lg">
|
||||
<p>Debug: Machine trouvée - {{ d.machine.value.name }}</p>
|
||||
<p>Components count: {{ d.components.value.length }}</p>
|
||||
<p>Pieces count: {{ d.pieces.value.length }}</p>
|
||||
</div>
|
||||
<!-- Debug info -->
|
||||
<div v-if="d.debug.value" class="bg-yellow-100 p-4 rounded-lg">
|
||||
<p>Debug: Machine trouvée - {{ d.machine.value.name }}</p>
|
||||
<p>Components count: {{ d.components.value.length }}</p>
|
||||
<p>Pieces count: {{ d.pieces.value.length }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<PageHero
|
||||
:title="d.machine.value.name"
|
||||
:subtitle="d.machine.value.description || d.machine.value.typeMachine?.description"
|
||||
min-height="min-h-[20vh]"
|
||||
max-width="max-w-md"
|
||||
rounded
|
||||
>
|
||||
<div class="flex justify-center gap-4">
|
||||
<div v-if="d.machine.value.typeMachine?.category" class="badge badge-outline">
|
||||
{{ d.machine.value.typeMachine?.category }}
|
||||
</div>
|
||||
<div v-if="d.machine.value.site?.name" class="badge badge-outline">
|
||||
{{ d.machine.value.site?.name }}
|
||||
</div>
|
||||
<div v-if="d.machine.value.reference" class="badge badge-outline">
|
||||
{{ d.machine.value.reference }}
|
||||
</div>
|
||||
<!-- Hero -->
|
||||
<PageHero
|
||||
:title="d.machine.value.name"
|
||||
:subtitle="d.machine.value.description"
|
||||
min-height="min-h-[20vh]"
|
||||
max-width="max-w-md"
|
||||
rounded
|
||||
>
|
||||
<div class="flex justify-center gap-4">
|
||||
<div v-if="d.machine.value.site?.name" class="badge badge-outline">
|
||||
{{ d.machine.value.site?.name }}
|
||||
</div>
|
||||
<div v-if="d.machine.value.reference" class="badge badge-outline">
|
||||
{{ d.machine.value.reference }}
|
||||
</div>
|
||||
</PageHero>
|
||||
|
||||
<!-- Machine Info Card -->
|
||||
<MachineInfoCard
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
:machine-name="d.machineName.value"
|
||||
:machine-reference="d.machineReference.value"
|
||||
:machine-constructeur-ids="d.machineConstructeurIds.value"
|
||||
:machine-constructeurs-display="d.machineConstructeursDisplay.value"
|
||||
:has-machine-constructeur="d.hasMachineConstructeur.value"
|
||||
:visible-custom-fields="d.visibleMachineCustomFields.value"
|
||||
:get-machine-field-id="d.getMachineFieldId"
|
||||
@update:machine-name="d.machineName.value = $event"
|
||||
@update:machine-reference="d.machineReference.value = $event"
|
||||
@update:constructeur-ids="d.handleMachineConstructeurChange"
|
||||
@blur-field="d.updateMachineInfo"
|
||||
@set-custom-field-value="d.setMachineCustomFieldValue"
|
||||
@update-custom-field="d.updateMachineCustomField"
|
||||
/>
|
||||
|
||||
<!-- Documents -->
|
||||
<MachineDocumentsCard
|
||||
:documents="d.machineDocumentsList.value"
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
:uploading="d.machineDocumentsUploading.value"
|
||||
:files="d.machineDocumentFiles.value"
|
||||
@update:files="d.machineDocumentFiles.value = $event"
|
||||
@files-added="d.handleMachineFilesAdded"
|
||||
@preview="d.openPreview"
|
||||
@download="d.downloadDocument"
|
||||
@remove="d.removeMachineDocument"
|
||||
/>
|
||||
|
||||
<!-- Produits associés -->
|
||||
<MachineProductsCard :products="d.machineDirectProducts.value" />
|
||||
|
||||
<!-- Components Section -->
|
||||
<MachineComponentsCard
|
||||
:components="d.components.value"
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
:collapsed="d.componentsCollapsed.value"
|
||||
:collapse-toggle-token="d.collapseToggleToken.value"
|
||||
@toggle-collapse="d.toggleAllComponents"
|
||||
@update-component="d.updateComponent"
|
||||
@edit-piece="d.updatePieceFromComponent"
|
||||
@custom-field-update="d.updatePieceCustomField"
|
||||
/>
|
||||
|
||||
<!-- Machine Pieces Section -->
|
||||
<MachinePiecesCard
|
||||
:pieces="d.machinePieces.value"
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
@update-piece="d.updatePieceInfo"
|
||||
@edit-piece="d.editPiece"
|
||||
@custom-field-update="d.updatePieceCustomField"
|
||||
/>
|
||||
|
||||
<!-- Comments -->
|
||||
<div class="mt-4">
|
||||
<CommentSection
|
||||
entity-type="machine"
|
||||
:entity-id="String(machineId)"
|
||||
:entity-name="d.machine.value?.name"
|
||||
show-resolved
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</PageHero>
|
||||
|
||||
<template v-else>
|
||||
<MachineSkeletonSummary
|
||||
:component-requirement-groups="d.componentRequirementGroups.value"
|
||||
:piece-requirement-groups="d.pieceRequirementGroups.value"
|
||||
:product-requirement-groups="d.productRequirementGroups.value"
|
||||
<!-- Machine Info Card -->
|
||||
<MachineInfoCard
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
:machine-name="d.machineName.value"
|
||||
:machine-reference="d.machineReference.value"
|
||||
:machine-constructeur-ids="d.machineConstructeurIds.value"
|
||||
:machine-constructeurs-display="d.machineConstructeursDisplay.value"
|
||||
:has-machine-constructeur="d.hasMachineConstructeur.value"
|
||||
:visible-custom-fields="d.visibleMachineCustomFields.value"
|
||||
:get-machine-field-id="d.getMachineFieldId"
|
||||
@update:machine-name="d.machineName.value = $event"
|
||||
@update:machine-reference="d.machineReference.value = $event"
|
||||
@update:constructeur-ids="d.handleMachineConstructeurChange"
|
||||
@blur-field="d.updateMachineInfo"
|
||||
@set-custom-field-value="d.setMachineCustomFieldValue"
|
||||
@update-custom-field="d.updateMachineCustomField"
|
||||
/>
|
||||
|
||||
<!-- Documents -->
|
||||
<MachineDocumentsCard
|
||||
:documents="d.machineDocumentsList.value"
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
:uploading="d.machineDocumentsUploading.value"
|
||||
:files="d.machineDocumentFiles.value"
|
||||
@update:files="d.machineDocumentFiles.value = $event"
|
||||
@files-added="d.handleMachineFilesAdded"
|
||||
@preview="d.openPreview"
|
||||
@download="d.downloadDocument"
|
||||
@remove="d.removeMachineDocument"
|
||||
/>
|
||||
|
||||
<!-- Produits associés -->
|
||||
<MachineProductsCard
|
||||
:products="d.machineDirectProducts.value"
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
@add-product="openAddModal('product')"
|
||||
@remove-product="d.removeProductLink"
|
||||
/>
|
||||
|
||||
<!-- Components Section -->
|
||||
<MachineComponentsCard
|
||||
:components="d.components.value"
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
:collapsed="d.componentsCollapsed.value"
|
||||
:collapse-toggle-token="d.collapseToggleToken.value"
|
||||
@toggle-collapse="d.toggleAllComponents"
|
||||
@update-component="d.updateComponent"
|
||||
@edit-piece="d.updatePieceFromComponent"
|
||||
@custom-field-update="d.updatePieceCustomField"
|
||||
@add-component="openAddModal('component')"
|
||||
@remove-component="d.removeComponentLink"
|
||||
/>
|
||||
|
||||
<!-- Machine Pieces Section -->
|
||||
<MachinePiecesCard
|
||||
:pieces="d.machinePieces.value"
|
||||
:is-edit-mode="d.isEditMode.value"
|
||||
:collapsed="d.piecesCollapsed.value"
|
||||
:collapse-toggle-token="d.pieceCollapseToggleToken.value"
|
||||
@update-piece="d.updatePieceInfo"
|
||||
@edit-piece="d.editPiece"
|
||||
@custom-field-update="d.updatePieceCustomField"
|
||||
@add-piece="openAddModal('piece')"
|
||||
@remove-piece="d.removePieceLink"
|
||||
@toggle-collapse="d.toggleAllPieces"
|
||||
/>
|
||||
|
||||
<!-- Add Entity Modal -->
|
||||
<AddEntityToMachineModal
|
||||
:open="addModalOpen"
|
||||
:entity-kind="addModalKind"
|
||||
@close="addModalOpen = false"
|
||||
@confirm="handleAddEntity"
|
||||
/>
|
||||
|
||||
<!-- Comments -->
|
||||
<div class="mt-4">
|
||||
<CommentSection
|
||||
entity-type="machine"
|
||||
:entity-id="String(machineId)"
|
||||
:entity-name="d.machine.value?.name"
|
||||
show-resolved
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
@@ -156,10 +159,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMachineDetailData } from '~/composables/useMachineDetailData'
|
||||
import { useMachineSkeletonEditor } from '~/composables/useMachineSkeletonEditor'
|
||||
import DocumentPreviewModal from '~/components/DocumentPreviewModal.vue'
|
||||
import PageHero from '~/components/PageHero.vue'
|
||||
import MachinePrintSelectionModal from '~/components/MachinePrintSelectionModal.vue'
|
||||
@@ -169,7 +171,7 @@ import MachineDocumentsCard from '~/components/machine/MachineDocumentsCard.vue'
|
||||
import MachineProductsCard from '~/components/machine/MachineProductsCard.vue'
|
||||
import MachineComponentsCard from '~/components/machine/MachineComponentsCard.vue'
|
||||
import MachinePiecesCard from '~/components/machine/MachinePiecesCard.vue'
|
||||
import MachineSkeletonSummary from '~/components/machine/MachineSkeletonSummary.vue'
|
||||
import AddEntityToMachineModal from '~/components/machine/AddEntityToMachineModal.vue'
|
||||
import IconLucideAlertTriangle from '~icons/lucide/alert-triangle'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -182,41 +184,25 @@ if (!machineId) {
|
||||
|
||||
const d = useMachineDetailData(machineId)
|
||||
|
||||
const s = useMachineSkeletonEditor({
|
||||
machine: d.machine,
|
||||
components: d.components,
|
||||
pieces: d.pieces,
|
||||
machineComponentLinks: d.machineComponentLinks,
|
||||
machinePieceLinks: d.machinePieceLinks,
|
||||
machineProductLinks: d.machineProductLinks,
|
||||
machineType: d.machineType,
|
||||
machineHasSkeletonRequirements: d.machineHasSkeletonRequirements,
|
||||
componentRequirements: d.componentRequirements,
|
||||
pieceRequirements: d.pieceRequirements,
|
||||
productRequirements: d.productRequirements,
|
||||
componentTypeLabelMap: d.componentTypeLabelMap,
|
||||
pieceTypeLabelMap: d.pieceTypeLabelMap,
|
||||
productInventory: d.productInventory,
|
||||
flattenedComponents: d.flattenedComponents,
|
||||
machinePieces: d.machinePieces,
|
||||
machineDocumentsLoaded: d.machineDocumentsLoaded,
|
||||
findProductById: d.findProductById,
|
||||
findComponentById: d.findComponentById,
|
||||
findPieceById: d.findPieceById,
|
||||
transformCustomFields: d.transformCustomFields,
|
||||
transformComponentCustomFields: d.transformComponentCustomFields,
|
||||
applyMachineLinks: d.applyMachineLinks,
|
||||
collapseAllComponents: d.collapseAllComponents,
|
||||
initMachineFields: d.initMachineFields,
|
||||
collectPiecesForSkeleton: d.collectPiecesForSkeleton,
|
||||
constructeurs: d.constructeurs,
|
||||
loadProducts: d.loadProducts,
|
||||
reconfigureMachineSkeleton: d.reconfigureMachineSkeleton,
|
||||
toast: d.toast,
|
||||
})
|
||||
const addModalOpen = ref(false)
|
||||
const addModalKind = ref('component')
|
||||
|
||||
const openAddModal = (kind) => {
|
||||
addModalKind.value = kind
|
||||
addModalOpen.value = true
|
||||
}
|
||||
|
||||
const handleAddEntity = async (entityId) => {
|
||||
if (addModalKind.value === 'component') {
|
||||
await d.addComponentLink(entityId)
|
||||
} else if (addModalKind.value === 'piece') {
|
||||
await d.addPieceLink(entityId)
|
||||
} else {
|
||||
await d.addProductLink(entityId)
|
||||
}
|
||||
}
|
||||
|
||||
const machineViewTitle = computed(() => {
|
||||
if (s.isSkeletonView.value) return 'Squelette de la machine'
|
||||
return d.isEditMode.value ? 'Modification de la machine' : 'Détails de la machine'
|
||||
})
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<div class="card bg-base-100 shadow-lg mb-6">
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Site</span>
|
||||
@@ -29,29 +29,14 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Type de machine</span>
|
||||
<span class="label-text">Recherche</span>
|
||||
</label>
|
||||
<select v-model="selectedType" class="select select-bordered">
|
||||
<option value="">
|
||||
Tous les types
|
||||
</option>
|
||||
<option v-for="type in machineTypes" :key="type.id" :value="type.id">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Catégorie</span>
|
||||
</label>
|
||||
<select v-model="selectedCategory" class="select select-bordered">
|
||||
<option value="">
|
||||
Toutes les catégories
|
||||
</option>
|
||||
<option v-for="category in categories" :key="category" :value="category">
|
||||
{{ category }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Rechercher par nom ou référence..."
|
||||
class="input input-bordered"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,26 +73,14 @@
|
||||
<h3 class="card-title text-lg">
|
||||
{{ machine.name }}
|
||||
</h3>
|
||||
<div class="badge badge-primary badge-sm">
|
||||
{{ machine.typeMachine?.category || 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
{{ machine.description || machine.typeMachine?.description }}
|
||||
</p>
|
||||
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<IconLucideMapPin class="w-4 h-4 text-blue-500" aria-hidden="true" />
|
||||
<span class="text-gray-600">{{ machine.site?.name || 'Site inconnu' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<IconLucideSettings2 class="w-4 h-4 text-green-500" aria-hidden="true" />
|
||||
<span class="text-gray-600">{{ machine.typeMachine?.name || 'Type inconnu' }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="machine.reference" class="flex items-center gap-2">
|
||||
<IconLucideTag class="w-4 h-4 text-orange-500" aria-hidden="true" />
|
||||
<span class="text-gray-600">{{ machine.reference }}</span>
|
||||
@@ -136,44 +109,28 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useMachines } from '~/composables/useMachines'
|
||||
import { useSites } from '~/composables/useSites'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { humanizeError } from '~/shared/utils/errorMessages'
|
||||
import IconLucidePlus from '~icons/lucide/plus'
|
||||
import IconLucideFactory from '~icons/lucide/factory'
|
||||
import IconLucideMapPin from '~icons/lucide/map-pin'
|
||||
import IconLucideSettings2 from '~icons/lucide/settings-2'
|
||||
import IconLucideTag from '~icons/lucide/tag'
|
||||
|
||||
const { canEdit } = usePermissions()
|
||||
const { machines, loading, loadMachines, deleteMachine } = useMachines()
|
||||
const { sites, loadSites } = useSites()
|
||||
const { machineTypes, loadMachineTypes } = useMachineTypesApi()
|
||||
const toast = useToast()
|
||||
|
||||
const selectedSite = ref('')
|
||||
const selectedType = ref('')
|
||||
const selectedCategory = ref('')
|
||||
const searchQuery = ref('')
|
||||
|
||||
const categories = computed(() => {
|
||||
const cats = new Set()
|
||||
machineTypes.value.forEach((type) => {
|
||||
if (type.category) {
|
||||
cats.add(type.category)
|
||||
}
|
||||
})
|
||||
return Array.from(cats)
|
||||
})
|
||||
|
||||
// Enrichir les machines avec les objets site et typeMachine complets
|
||||
// Enrichir les machines avec les objets site complets
|
||||
const enrichedMachines = computed(() => {
|
||||
return machines.value.map((machine) => {
|
||||
const site = sites.value.find(s => s.id === machine.siteId)
|
||||
const typeMachine = machineTypes.value.find(t => t.id === machine.typeMachineId)
|
||||
return {
|
||||
...machine,
|
||||
site: site || null,
|
||||
typeMachine: typeMachine || null
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -185,12 +142,12 @@ const filteredMachines = computed(() => {
|
||||
filtered = filtered.filter(machine => machine.siteId === selectedSite.value)
|
||||
}
|
||||
|
||||
if (selectedType.value) {
|
||||
filtered = filtered.filter(machine => machine.typeMachineId === selectedType.value)
|
||||
}
|
||||
|
||||
if (selectedCategory.value) {
|
||||
filtered = filtered.filter(machine => machine.typeMachine?.category === selectedCategory.value)
|
||||
if (searchQuery.value.trim()) {
|
||||
const term = searchQuery.value.trim().toLowerCase()
|
||||
filtered = filtered.filter(machine =>
|
||||
machine.name?.toLowerCase().includes(term)
|
||||
|| machine.reference?.toLowerCase().includes(term),
|
||||
)
|
||||
}
|
||||
|
||||
return filtered
|
||||
@@ -227,7 +184,6 @@ onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadMachines(),
|
||||
loadSites(),
|
||||
loadMachineTypes()
|
||||
])
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">
|
||||
Nouvelle machine
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500">
|
||||
Renseignez les informations et la configuration avant de créer la machine.
|
||||
Renseignez les informations de base pour créer la machine.
|
||||
</p>
|
||||
</div>
|
||||
<NuxtLink to="/machines" class="btn btn-ghost">
|
||||
@@ -15,21 +15,25 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6" @submit.prevent="c.finalizeMachineCreation">
|
||||
<div v-if="c.loading" class="flex justify-center items-center py-12">
|
||||
<span class="loading loading-spinner loading-lg" />
|
||||
</div>
|
||||
|
||||
<form v-else class="space-y-6" @submit.prevent="c.finalizeMachineCreation">
|
||||
<div class="card bg-base-100 shadow-lg">
|
||||
<div class="card-body space-y-6">
|
||||
<!-- Basic fields -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="machine-field-name">
|
||||
<span class="label-text">Nom de la machine</span>
|
||||
<span class="label-text">Nom de la machine <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
id="machine-field-name"
|
||||
v-model="c.newMachine.name"
|
||||
type="text"
|
||||
placeholder="Ex: Presse hydraulique #1"
|
||||
class="input input-bordered"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit"
|
||||
required
|
||||
>
|
||||
@@ -37,9 +41,15 @@
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="machine-field-site">
|
||||
<span class="label-text">Site</span>
|
||||
<span class="label-text">Site <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<select id="machine-field-site" v-model="c.newMachine.siteId" class="select select-bordered" :disabled="!canEdit" required>
|
||||
<select
|
||||
id="machine-field-site"
|
||||
v-model="c.newMachine.siteId"
|
||||
class="select select-bordered select-sm md:select-md"
|
||||
:disabled="!canEdit"
|
||||
required
|
||||
>
|
||||
<option value="">
|
||||
Sélectionner un site
|
||||
</option>
|
||||
@@ -52,129 +62,43 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Type de machine</span>
|
||||
<label class="label" for="machine-field-reference">
|
||||
<span class="label-text">Référence</span>
|
||||
</label>
|
||||
<SearchSelect
|
||||
v-model="c.newMachine.typeMachineId"
|
||||
:options="c.machineTypes"
|
||||
:loading="c.machineTypesLoading"
|
||||
<input
|
||||
id="machine-field-reference"
|
||||
v-model="c.newMachine.reference"
|
||||
type="text"
|
||||
placeholder="Ex: PRESS-001"
|
||||
class="input input-bordered input-sm md:input-md"
|
||||
:disabled="!canEdit"
|
||||
placeholder="Rechercher un type…"
|
||||
empty-text="Aucun type trouvé"
|
||||
:option-label="c.machineTypeLabel"
|
||||
:option-description="c.machineTypeDescription"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Référence</span>
|
||||
<span class="label-text">Cloner depuis une machine</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="c.newMachine.reference"
|
||||
type="text"
|
||||
placeholder="Ex: PRESS-001"
|
||||
class="input input-bordered"
|
||||
<SearchSelect
|
||||
v-model="c.newMachine.cloneFromMachineId"
|
||||
:options="c.machines"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
:clearable="true"
|
||||
placeholder="Rechercher une machine..."
|
||||
empty-text="Aucune machine trouvée"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type structure summary -->
|
||||
<div v-if="c.selectedMachineType" class="p-4 bg-gray-50 rounded-lg space-y-2 text-sm">
|
||||
<h4 class="font-semibold text-sm">
|
||||
Structure du type sélectionné :
|
||||
</h4>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="font-medium">Familles de composants :</span>
|
||||
<span class="badge badge-sm">{{ c.selectedMachineType.componentRequirements?.length || 0 }}</span>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="font-medium">Groupes de pièces :</span>
|
||||
<span class="badge badge-sm">{{ c.selectedMachineType.pieceRequirements?.length || 0 }}</span>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="font-medium">Produits requis :</span>
|
||||
<span class="badge badge-sm">{{ c.selectedMachineType.productRequirements?.length || 0 }}</span>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="font-medium">Catégorie :</span>
|
||||
<span class="badge badge-outline badge-sm">{{ c.selectedMachineType.category || 'N/A' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="(c.selectedMachineType.componentRequirements?.length || 0) === 0 && (c.selectedMachineType.pieceRequirements?.length || 0) === 0 && (c.selectedMachineType.productRequirements?.length || 0) === 0"
|
||||
class="text-xs text-gray-500"
|
||||
>
|
||||
Ce type n'a pas encore de familles configurées. La machine héritera de la structure legacy du type.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Requirement selectors -->
|
||||
<RequirementComponentSelector
|
||||
:requirements="c.selectedMachineType?.componentRequirements || []"
|
||||
:loading="c.composantsLoading"
|
||||
:get-entries="c.getComponentRequirementEntries"
|
||||
:get-options="c.getComponentOptions"
|
||||
:resolve-type-label="c.resolveComponentRequirementTypeLabel"
|
||||
:find-by-id="c.findComponentById"
|
||||
:option-label="c.componentOptionLabel"
|
||||
:option-description="c.componentOptionDescription"
|
||||
@add-entry="c.addComponentSelectionEntry"
|
||||
@remove-entry="c.removeComponentSelectionEntry"
|
||||
@set-component="c.setComponentRequirementComponent"
|
||||
/>
|
||||
|
||||
<RequirementPieceSelector
|
||||
:requirements="c.selectedMachineType?.pieceRequirements || []"
|
||||
:loading="c.piecesLoading"
|
||||
:piece-loading-by-key="c.pieceLoadingByKey"
|
||||
:get-entries="c.getPieceRequirementEntries"
|
||||
:get-options="c.getPieceOptions"
|
||||
:get-piece-key="c.getPieceKey"
|
||||
:resolve-type-label="c.resolvePieceRequirementTypeLabel"
|
||||
:find-by-id="c.findPieceById"
|
||||
:option-label="c.pieceOptionLabel"
|
||||
:option-description="c.pieceOptionDescription"
|
||||
@add-entry="c.addPieceSelectionEntry"
|
||||
@remove-entry="c.removePieceSelectionEntry"
|
||||
@set-piece="c.setPieceRequirementPiece"
|
||||
@search="c.fetchPieceOptions"
|
||||
/>
|
||||
|
||||
<RequirementProductSelector
|
||||
:requirements="c.selectedMachineType?.productRequirements || []"
|
||||
:products-loading="c.productsLoading"
|
||||
:get-entries="c.getProductRequirementEntries"
|
||||
:get-product-options="c.getProductOptions"
|
||||
:find-by-id="c.findProductById"
|
||||
@add-entry="c.addProductSelectionEntry"
|
||||
@remove-entry="c.removeProductSelectionEntry"
|
||||
@set-product="c.setProductRequirementProduct"
|
||||
/>
|
||||
|
||||
<!-- Preview -->
|
||||
<MachineCreatePreview :preview="c.machinePreview" />
|
||||
|
||||
<!-- Blocking issues warning -->
|
||||
<div
|
||||
v-if="c.blockingPreviewIssues.length"
|
||||
class="text-xs text-error bg-error/10 border border-error/20 rounded-md px-3 py-2"
|
||||
>
|
||||
Compléter les informations bloquantes avant de créer la machine.
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-base-200">
|
||||
<NuxtLink to="/machines" class="btn btn-outline">
|
||||
<NuxtLink to="/machines" class="btn btn-outline btn-sm md:btn-md">
|
||||
Annuler
|
||||
</NuxtLink>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="!canEdit || !c.canCreateMachine || c.submitting"
|
||||
class="btn btn-sm md:btn-md btn-primary"
|
||||
:disabled="!canEdit || !c.newMachine.name?.trim() || c.submitting"
|
||||
:class="{ loading: c.submitting }"
|
||||
>
|
||||
Créer la machine
|
||||
@@ -191,10 +115,6 @@
|
||||
import { proxyRefs } from 'vue'
|
||||
import { useMachineCreatePage } from '~/composables/useMachineCreatePage'
|
||||
import SearchSelect from '~/components/common/SearchSelect.vue'
|
||||
import RequirementComponentSelector from '~/components/machine/create/RequirementComponentSelector.vue'
|
||||
import RequirementPieceSelector from '~/components/machine/create/RequirementPieceSelector.vue'
|
||||
import RequirementProductSelector from '~/components/machine/create/RequirementProductSelector.vue'
|
||||
import MachineCreatePreview from '~/components/machine/create/MachineCreatePreview.vue'
|
||||
|
||||
const c = proxyRefs(useMachineCreatePage())
|
||||
const { canEdit } = usePermissions()
|
||||
|
||||
@@ -945,7 +945,6 @@ const submitEdition = async () => {
|
||||
updatedPiece.id,
|
||||
[
|
||||
updatedPiece?.typePiece?.pieceCustomFields,
|
||||
updatedPiece?.typeMachinePieceRequirement?.typePiece?.pieceCustomFields,
|
||||
],
|
||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||
)
|
||||
|
||||
@@ -573,7 +573,6 @@ const submitCreation = async () => {
|
||||
createdPiece.id,
|
||||
[
|
||||
createdPiece?.typePiece?.pieceCustomFields,
|
||||
createdPiece?.typeMachinePieceRequirement?.typePiece?.pieceCustomFields,
|
||||
],
|
||||
{ customFieldInputs, upsertCustomFieldValue, updateCustomFieldValue, toast },
|
||||
)
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="my-8 text-center">
|
||||
<div class="loading loading-spinner loading-lg" />
|
||||
<p class="mt-4 text-gray-600">
|
||||
Chargement du type...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Type Details -->
|
||||
<div v-else-if="type" class="my-8">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
{{ typePageTitle }}
|
||||
</h2>
|
||||
<div class="flex gap-2">
|
||||
<NuxtLink :to="`/type/edit/${type.id}`" class="btn btn-secondary">
|
||||
<IconLucideSquarePen class="w-4 h-4 mr-2" aria-hidden="true" />
|
||||
Éditer complètement
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/machine-skeleton" class="btn btn-outline">
|
||||
Retour
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Type Info -->
|
||||
<TypeInfoDisplay :type="type" />
|
||||
|
||||
<!-- Familles de composants -->
|
||||
<div v-if="componentRequirementCount > 0" class="mb-8 space-y-3">
|
||||
<h3 class="text-lg font-semibold">
|
||||
Familles de composants
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="requirement in type.componentRequirements"
|
||||
:key="requirement.id"
|
||||
class="border border-base-200 rounded-lg p-4 bg-base-100"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold">
|
||||
{{ requirement.label || requirement.typeComposant?.name || 'Famille' }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500">
|
||||
Type : {{ requirement.typeComposant?.name || 'Non défini' }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="badge badge-outline badge-sm">
|
||||
Min {{ toDisplayCount(requirement.minCount, requirement.required ? 1 : 0) }} •
|
||||
Max {{ toDisplayCount(requirement.maxCount, '∞') }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
{{ requirement.allowNewModels ? 'Création de modèles autorisée' : 'Modèles existants uniquement' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Groupes de pièces -->
|
||||
<div v-if="pieceRequirementCount > 0" class="mb-8 space-y-3">
|
||||
<h3 class="text-lg font-semibold">
|
||||
Groupes de pièces
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="requirement in type.pieceRequirements"
|
||||
:key="requirement.id"
|
||||
class="border border-base-200 rounded-lg p-4 bg-base-100"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold">
|
||||
{{ requirement.label || requirement.typePiece?.name || 'Groupe' }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500">
|
||||
Type : {{ requirement.typePiece?.name || 'Non défini' }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="badge badge-outline badge-sm">
|
||||
Min {{ toDisplayCount(requirement.minCount, requirement.required ? 1 : 0) }} •
|
||||
Max {{ toDisplayCount(requirement.maxCount, '∞') }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
{{ requirement.allowNewModels ? 'Création de modèles autorisée' : 'Modèles existants uniquement' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Produits requis -->
|
||||
<div v-if="productRequirementCount > 0" class="mb-8 space-y-3">
|
||||
<h3 class="text-lg font-semibold">
|
||||
Produits requis
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="requirement in type.productRequirements"
|
||||
:key="requirement.id || requirement.typeProductId"
|
||||
class="border border-base-200 rounded-lg p-4 bg-base-100"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold">
|
||||
{{ requirement.label || requirement.typeProduct?.name || 'Produit' }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500">
|
||||
Type : {{ requirement.typeProduct?.name || 'Non défini' }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="badge badge-outline badge-sm">
|
||||
Min {{ toDisplayCount(requirement.minCount, requirement.required ? 1 : 0) }} •
|
||||
Max {{ toDisplayCount(requirement.maxCount, '∞') }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
{{ requirement.allowNewModels ? 'Création de produits autorisée' : 'Produits existants uniquement' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Commentaires -->
|
||||
<CommentSection
|
||||
entity-type="machine_skeleton"
|
||||
:entity-id="type.id"
|
||||
:entity-name="type.name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else class="my-8 text-center">
|
||||
<div class="alert alert-error">
|
||||
<div>
|
||||
<h3 class="font-bold">
|
||||
Type non trouvé
|
||||
</h3>
|
||||
<p>Le type de machine demandé n'existe pas.</p>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink to="/machine-skeleton" class="btn btn-primary mt-4">
|
||||
Retour aux types
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import IconLucideSquarePen from '~icons/lucide/square-pen'
|
||||
|
||||
const { canEdit } = usePermissions()
|
||||
const route = useRoute()
|
||||
const { getMachineTypeById } = useMachineTypesApi()
|
||||
const { showError } = useToast()
|
||||
|
||||
const type = ref(null)
|
||||
const loading = ref(true)
|
||||
|
||||
const isEditView = computed(() => {
|
||||
const editQuery = Array.isArray(route.query.edit) ? route.query.edit[0] : route.query.edit
|
||||
const modeQuery = Array.isArray(route.query.mode) ? route.query.mode[0] : route.query.mode
|
||||
return editQuery === 'true' || editQuery === '1' || modeQuery === 'edit'
|
||||
})
|
||||
|
||||
const typePageTitle = computed(() => {
|
||||
const currentName = type.value?.name || 'Type de squelette'
|
||||
return isEditView.value ? `Modification : ${currentName}` : currentName
|
||||
})
|
||||
|
||||
const componentRequirementCount = computed(() => type.value?.componentRequirements?.length || 0)
|
||||
const pieceRequirementCount = computed(() => type.value?.pieceRequirements?.length || 0)
|
||||
const productRequirementCount = computed(() => type.value?.productRequirements?.length || 0)
|
||||
|
||||
const toDisplayCount = (value, fallback) => {
|
||||
if (value === null || value === undefined) {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const typeId = route.params.id
|
||||
if (!typeId) {
|
||||
showError('Aucun identifiant de type fourni')
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const result = await getMachineTypeById(typeId)
|
||||
if (result.success) {
|
||||
type.value = result.data
|
||||
} else {
|
||||
showError('Type non trouvé')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement:', error)
|
||||
showError('Erreur lors du chargement')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,265 +0,0 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="my-8 text-center">
|
||||
<div class="loading loading-spinner loading-lg" />
|
||||
<p class="mt-4 text-gray-600">
|
||||
Chargement du type...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Locked: machines linked -->
|
||||
<div v-else-if="type && hasMachines" class="my-8">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
{{ type.name }}
|
||||
</h2>
|
||||
<NuxtLink to="/machine-skeleton" class="btn btn-outline">
|
||||
Retour
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<IconLucideTriangleAlert class="w-5 h-5" />
|
||||
<span>Ce squelette ne peut pas être modifié car des machines y sont rattachées.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<div v-else-if="type" class="my-8">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
Modifier : {{ type.name }}
|
||||
</h2>
|
||||
<NuxtLink to="/machine-skeleton" class="btn btn-outline">
|
||||
Retour
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<TypeEditForm
|
||||
v-model="editedType"
|
||||
:saving="saving"
|
||||
@submit="saveChanges"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else class="my-8 text-center">
|
||||
<div class="alert alert-error">
|
||||
<div>
|
||||
<h3 class="font-bold">
|
||||
Type non trouvé
|
||||
</h3>
|
||||
<p>Le type de machine demandé n'existe pas.</p>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink to="/machine-skeleton" class="btn btn-primary mt-4">
|
||||
Retour aux types
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useMachineTypesApi } from '~/composables/useMachineTypesApi'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { extractRelationId } from '~/shared/apiRelations'
|
||||
import IconLucideTriangleAlert from '~icons/lucide/triangle-alert'
|
||||
|
||||
const { canEdit } = usePermissions()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { getMachineTypeById, updateMachineType } = useMachineTypesApi()
|
||||
const { showSuccess, showError } = useToast()
|
||||
|
||||
const type = ref(null)
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const hasMachines = computed(() => {
|
||||
const machines = type.value?.machines
|
||||
return Array.isArray(machines) && machines.length > 0
|
||||
})
|
||||
|
||||
// Données éditées du type
|
||||
const editedType = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
category: '',
|
||||
maintenanceFrequency: '',
|
||||
customFields: [],
|
||||
componentRequirements: [],
|
||||
pieceRequirements: [],
|
||||
productRequirements: []
|
||||
})
|
||||
|
||||
const parseOptions = (field = {}) => {
|
||||
if (field.type !== 'select') { return [] }
|
||||
if (field.optionsText && typeof field.optionsText === 'string') {
|
||||
return field.optionsText
|
||||
.split('\n')
|
||||
.map(option => option.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
if (Array.isArray(field.options)) {
|
||||
return field.options
|
||||
.map(option => String(option).trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const toModelTypeIri = (value) => {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
if (typeof value === 'string' && value.startsWith('/api/model_types/')) {
|
||||
return value
|
||||
}
|
||||
const relationId = extractRelationId(value)
|
||||
if (relationId) {
|
||||
return `/api/model_types/${relationId}`
|
||||
}
|
||||
return typeof value === 'string' ? `/api/model_types/${value}` : undefined
|
||||
}
|
||||
|
||||
const normalizeCustomFields = (fields = []) =>
|
||||
fields
|
||||
.filter(field => field?.name && field.name.trim() !== '')
|
||||
.map((field, index) => ({
|
||||
name: field.name,
|
||||
type: field.type || '',
|
||||
required: !!field.required,
|
||||
options: parseOptions(field),
|
||||
orderIndex: typeof field.orderIndex === 'number' ? field.orderIndex : index
|
||||
}))
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
.map((field, index) => ({ ...field, orderIndex: index }))
|
||||
|
||||
const toIntegerOrNull = (value, fallback = null) => {
|
||||
if (value === '' || value === undefined || value === null) {
|
||||
return fallback
|
||||
}
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
const normalizeComponentRequirements = (requirements = []) =>
|
||||
requirements
|
||||
.filter(req => req?.typeComposantId || req?.typeComposant)
|
||||
.map((req, index) => ({
|
||||
typeComposant: toModelTypeIri(req.typeComposantId || req.typeComposant),
|
||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||
minCount: toIntegerOrNull(req.minCount, 1),
|
||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||
required: req.required ?? true,
|
||||
allowNewModels: req.allowNewModels ?? true,
|
||||
orderIndex: typeof req.orderIndex === 'number' ? req.orderIndex : index
|
||||
}))
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
.map((req, index) => ({ ...req, orderIndex: index }))
|
||||
|
||||
const normalizePieceRequirements = (requirements = []) =>
|
||||
requirements
|
||||
.filter(req => req?.typePieceId || req?.typePiece)
|
||||
.map((req, index) => ({
|
||||
typePiece: toModelTypeIri(req.typePieceId || req.typePiece),
|
||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||
minCount: toIntegerOrNull(req.minCount, 0),
|
||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||
required: req.required ?? false,
|
||||
allowNewModels: req.allowNewModels ?? true,
|
||||
orderIndex: typeof req.orderIndex === 'number' ? req.orderIndex : index
|
||||
}))
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
.map((req, index) => ({ ...req, orderIndex: index }))
|
||||
|
||||
const normalizeProductRequirements = (requirements = []) =>
|
||||
requirements
|
||||
.filter(req => req?.typeProductId || req?.typeProduct)
|
||||
.map((req, index) => ({
|
||||
typeProduct: toModelTypeIri(req.typeProductId || req.typeProduct),
|
||||
label: req.label?.trim() ? req.label.trim() : undefined,
|
||||
minCount: toIntegerOrNull(req.minCount, 0),
|
||||
maxCount: toIntegerOrNull(req.maxCount, null),
|
||||
required: req.required ?? false,
|
||||
allowNewModels: req.allowNewModels ?? true,
|
||||
orderIndex: typeof req.orderIndex === 'number' ? req.orderIndex : index
|
||||
}))
|
||||
.sort((a, b) => (a.orderIndex ?? 0) - (b.orderIndex ?? 0))
|
||||
.map((req, index) => ({ ...req, orderIndex: index }))
|
||||
|
||||
const saveChanges = async () => {
|
||||
try {
|
||||
saving.value = true
|
||||
|
||||
const currentEditedType = editedType.value
|
||||
|
||||
// Préparer les données pour l'API
|
||||
const updatedType = {
|
||||
...currentEditedType,
|
||||
customFields: normalizeCustomFields(currentEditedType.customFields),
|
||||
componentRequirements: normalizeComponentRequirements(currentEditedType.componentRequirements),
|
||||
pieceRequirements: normalizePieceRequirements(currentEditedType.pieceRequirements),
|
||||
productRequirements: normalizeProductRequirements(currentEditedType.productRequirements)
|
||||
}
|
||||
|
||||
const result = await updateMachineType(type.value.id, updatedType)
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Type mis à jour avec succès !')
|
||||
router.push('/machine-skeleton')
|
||||
} else {
|
||||
showError('Erreur lors de la mise à jour du type')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde:', error)
|
||||
showError('Erreur lors de la sauvegarde')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Charger le type au montage
|
||||
onMounted(async () => {
|
||||
if (!canEdit.value) {
|
||||
router.replace(`/type/${route.params.id}`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const typeId = route.params.id
|
||||
const result = await getMachineTypeById(typeId, true)
|
||||
if (result.success) {
|
||||
type.value = result.data
|
||||
|
||||
// Initialiser les données éditées
|
||||
editedType.value = {
|
||||
name: type.value.name || '',
|
||||
description: type.value.description || '',
|
||||
category: type.value.category || '',
|
||||
maintenanceFrequency: type.value.maintenanceFrequency || '',
|
||||
customFields: type.value.customFields || [],
|
||||
componentRequirements: type.value.componentRequirements || [],
|
||||
pieceRequirements: type.value.pieceRequirements || [],
|
||||
productRequirements: type.value.productRequirements || [],
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to load type:', result.error)
|
||||
showError('Type non trouvé')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement:', error)
|
||||
showError('Erreur lors du chargement')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user