Compare commits
3 Commits
5b9c4ca09d
...
v1.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0124c11ba | ||
|
|
7e67b124f3 | ||
| 3ad326348b |
@@ -56,6 +56,57 @@
|
|||||||
--noise: 0;
|
--noise: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "mytheme-dark";
|
||||||
|
default: false;
|
||||||
|
prefersdark: true;
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
/* Surfaces — dark blue-gray */
|
||||||
|
--color-base-100: oklch(22% 0.015 260);
|
||||||
|
--color-base-200: oklch(18% 0.012 260);
|
||||||
|
--color-base-300: oklch(28% 0.018 260);
|
||||||
|
--color-base-content: oklch(92% 0.005 260);
|
||||||
|
|
||||||
|
/* Primary — Malio blue, brighter for dark */
|
||||||
|
--color-primary: oklch(55% 0.18 262);
|
||||||
|
--color-primary-content: oklch(98% 0.005 262);
|
||||||
|
|
||||||
|
/* Secondary — refined lavender */
|
||||||
|
--color-secondary: oklch(72% 0.06 275);
|
||||||
|
--color-secondary-content: oklch(22% 0.03 275);
|
||||||
|
|
||||||
|
/* Accent — warm amber-orange */
|
||||||
|
--color-accent: oklch(72% 0.17 55);
|
||||||
|
--color-accent-content: oklch(20% 0.04 55);
|
||||||
|
|
||||||
|
/* Neutral — lighter slate for dark mode */
|
||||||
|
--color-neutral: oklch(75% 0.02 260);
|
||||||
|
--color-neutral-content: oklch(18% 0.01 260);
|
||||||
|
|
||||||
|
/* Semantic */
|
||||||
|
--color-info: oklch(62% 0.14 255);
|
||||||
|
--color-info-content: oklch(98% 0.005 255);
|
||||||
|
--color-success: oklch(65% 0.19 150);
|
||||||
|
--color-success-content: oklch(98% 0.005 150);
|
||||||
|
--color-warning: oklch(78% 0.15 70);
|
||||||
|
--color-warning-content: oklch(22% 0.05 70);
|
||||||
|
--color-error: oklch(62% 0.22 25);
|
||||||
|
--color-error-content: oklch(98% 0.005 25);
|
||||||
|
|
||||||
|
/* Geometry — same as light */
|
||||||
|
--radius-selector: 0.75rem;
|
||||||
|
--radius-field: 0.375rem;
|
||||||
|
--radius-box: 0.625rem;
|
||||||
|
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Typography ─── */
|
/* ─── Typography ─── */
|
||||||
:root {
|
:root {
|
||||||
--font-heading: 'Outfit', system-ui, sans-serif;
|
--font-heading: 'Outfit', system-ui, sans-serif;
|
||||||
@@ -139,6 +190,18 @@ h1, h2, h3, h4, h5, h6,
|
|||||||
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-card {
|
||||||
|
background-color: oklch(100% 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="mytheme-dark"] .site-card {
|
||||||
|
background-color: oklch(24% 0.015 260);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="mytheme-dark"] .card {
|
||||||
|
border-color: oklch(30% 0.02 260 / 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 6px -1px oklch(22% 0.025 260 / 0.06),
|
0 4px 6px -1px oklch(22% 0.025 260 / 0.06),
|
||||||
@@ -153,6 +216,11 @@ h1, h2, h3, h4, h5, h6,
|
|||||||
border-bottom: 1px solid oklch(91% 0.015 260 / 0.5);
|
border-bottom: 1px solid oklch(91% 0.015 260 / 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="mytheme-dark"] .navbar-glass {
|
||||||
|
background: oklch(22% 0.015 260 / 0.85);
|
||||||
|
border-bottom-color: oklch(30% 0.02 260 / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Buttons ─── */
|
/* ─── Buttons ─── */
|
||||||
.btn {
|
.btn {
|
||||||
font-family: var(--font-heading);
|
font-family: var(--font-heading);
|
||||||
|
|||||||
@@ -5,11 +5,13 @@
|
|||||||
<ComponentItem
|
<ComponentItem
|
||||||
:component="component"
|
:component="component"
|
||||||
:is-edit-mode="isEditMode"
|
:is-edit-mode="isEditMode"
|
||||||
|
:show-delete="showDelete"
|
||||||
:collapse-all="collapseAll"
|
:collapse-all="collapseAll"
|
||||||
:toggle-token="toggleToken"
|
:toggle-token="toggleToken"
|
||||||
@update="$emit('update', $event)"
|
@update="$emit('update', $event)"
|
||||||
@edit-piece="$emit('edit-piece', $event)"
|
@edit-piece="$emit('edit-piece', $event)"
|
||||||
@custom-field-update="$emit('custom-field-update', $event)"
|
@custom-field-update="$emit('custom-field-update', $event)"
|
||||||
|
@delete="$emit('delete')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,6 +29,10 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
showDelete: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
collapseAll: {
|
collapseAll: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
@@ -37,5 +43,5 @@ defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['update', 'edit-piece', 'custom-field-update'])
|
defineEmits(['update', 'edit-piece', 'custom-field-update', 'delete'])
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<div>
|
||||||
<DocumentPreviewModal
|
<DocumentPreviewModal
|
||||||
:document="previewDocument"
|
:document="previewDocument"
|
||||||
:visible="previewVisible"
|
:visible="previewVisible"
|
||||||
@@ -8,160 +8,135 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Component Header -->
|
<!-- Component Header -->
|
||||||
<div class="flex items-start justify-between p-4 bg-base-200 rounded-lg">
|
<div class="flex items-center gap-3 p-3 bg-base-200 rounded-lg cursor-pointer" @click="toggleCollapse">
|
||||||
<div class="flex items-start gap-3 w-full">
|
<IconLucideChevronRight
|
||||||
<button
|
class="w-4 h-4 shrink-0 transition-transform text-base-content/50"
|
||||||
type="button"
|
|
||||||
class="btn btn-ghost btn-sm btn-circle shrink-0 transition-transform"
|
|
||||||
:class="{ 'rotate-90': !isCollapsed }"
|
:class="{ 'rotate-90': !isCollapsed }"
|
||||||
:aria-expanded="!isCollapsed"
|
aria-hidden="true"
|
||||||
:title="isCollapsed ? 'Déplier les détails du composant' : 'Replier les détails du composant'"
|
/>
|
||||||
@click="toggleCollapse"
|
<div class="flex-1 min-w-0">
|
||||||
>
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<IconLucideChevronRight class="w-5 h-5 transition-transform" aria-hidden="true" />
|
<h3 class="text-sm font-semibold text-base-content truncate">
|
||||||
<span class="sr-only">{{ isCollapsed ? 'Déplier' : 'Replier' }} le composant</span>
|
|
||||||
</button>
|
|
||||||
<div class="flex-1">
|
|
||||||
<h3 class="text-lg font-semibold">
|
|
||||||
{{ component.name }}
|
{{ component.name }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex flex-wrap gap-2 mt-2">
|
<span v-if="component.reference" class="badge badge-outline badge-xs">{{ component.reference }}</span>
|
||||||
<span v-if="component.reference" class="badge badge-outline badge-sm">{{ component.reference }}</span>
|
<span v-if="component.prix" class="badge badge-primary badge-xs">{{ component.prix }}€</span>
|
||||||
<template v-if="componentConstructeursDisplay.length">
|
</div>
|
||||||
|
<div v-if="componentConstructeursDisplay.length || displayProductName" class="flex flex-wrap gap-1.5 mt-1">
|
||||||
<span
|
<span
|
||||||
v-for="constructeur in componentConstructeursDisplay"
|
v-for="constructeur in componentConstructeursDisplay"
|
||||||
:key="constructeur.id"
|
:key="constructeur.id"
|
||||||
class="badge badge-outline badge-sm"
|
class="text-xs text-base-content/50"
|
||||||
>
|
>
|
||||||
{{ constructeur.name }}
|
{{ constructeur.name }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
<span v-if="displayProductName" class="badge badge-info badge-xs">
|
||||||
<span v-if="component.prix" class="badge badge-primary badge-sm">{{ component.prix }}€</span>
|
{{ displayProductName }}
|
||||||
<span
|
|
||||||
v-if="displayProductName"
|
|
||||||
class="badge badge-info badge-sm"
|
|
||||||
>
|
|
||||||
Produit : {{ displayProductName }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button
|
||||||
|
v-if="showDelete"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs text-error shrink-0"
|
||||||
|
title="Supprimer ce composant"
|
||||||
|
@click.stop="$emit('delete')"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="!isCollapsed" class="space-y-4">
|
<!-- Expanded content -->
|
||||||
<!-- Component Info Display - Editable or Read-only -->
|
<div v-show="!isCollapsed" class="mt-3 space-y-4 pl-7">
|
||||||
<div class="p-4 bg-base-100 border border-base-200 rounded-lg">
|
<!-- Info fields -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
<div v-if="isEditMode" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label"><span class="label-text font-medium">Nom</span></label>
|
<label class="label py-1"><span class="label-text text-xs font-medium text-base-content/60">Nom</span></label>
|
||||||
<input
|
<input v-model="component.name" type="text" class="input input-bordered input-sm" @blur="updateComponent">
|
||||||
v-if="isEditMode"
|
|
||||||
v-model="component.name"
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered input-sm"
|
|
||||||
@blur="updateComponent"
|
|
||||||
>
|
|
||||||
<div v-else class="input input-bordered input-sm bg-base-200">
|
|
||||||
{{ component.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label"><span class="label-text font-medium">Référence</span></label>
|
<label class="label py-1"><span class="label-text text-xs font-medium text-base-content/60">Référence</span></label>
|
||||||
<input
|
<input v-model="component.reference" type="text" class="input input-bordered input-sm" @blur="updateComponent">
|
||||||
v-if="isEditMode"
|
|
||||||
v-model="component.reference"
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered input-sm"
|
|
||||||
@blur="updateComponent"
|
|
||||||
>
|
|
||||||
<div v-else class="input input-bordered input-sm bg-base-200">
|
|
||||||
{{ component.reference || 'Non définie' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label"><span class="label-text font-medium">Prix</span></label>
|
<label class="label py-1"><span class="label-text text-xs font-medium text-base-content/60">Prix</span></label>
|
||||||
<input
|
<input v-model="component.prix" type="number" step="0.01" class="input input-bordered input-sm" @blur="updateComponent">
|
||||||
v-if="isEditMode"
|
|
||||||
v-model="component.prix"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
class="input input-bordered input-sm"
|
|
||||||
@blur="updateComponent"
|
|
||||||
>
|
|
||||||
<div v-else class="input input-bordered input-sm bg-base-200">
|
|
||||||
{{ component.prix ? `${component.prix}€` : 'Non défini' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label"><span class="label-text font-medium">Fournisseur</span></label>
|
<label class="label py-1"><span class="label-text text-xs font-medium text-base-content/60">Fournisseur</span></label>
|
||||||
<ConstructeurSelect
|
<ConstructeurSelect
|
||||||
v-if="isEditMode"
|
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:model-value="componentConstructeurIds"
|
:model-value="componentConstructeurIds"
|
||||||
:initial-options="componentConstructeursDisplay"
|
:initial-options="componentConstructeursDisplay"
|
||||||
@update:model-value="handleConstructeurChange"
|
@update:model-value="handleConstructeurChange"
|
||||||
/>
|
/>
|
||||||
<div v-else class="input input-bordered input-sm bg-base-200">
|
</div>
|
||||||
<div v-if="componentConstructeursDisplay.length" class="space-y-1">
|
</div>
|
||||||
<div
|
|
||||||
|
<!-- Read-only info -->
|
||||||
|
<div v-else class="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-base-content/40 mb-0.5">Nom</p>
|
||||||
|
<p class="text-base-content">{{ component.name }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-base-content/40 mb-0.5">Référence</p>
|
||||||
|
<p class="text-base-content">{{ component.reference || '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-base-content/40 mb-0.5">Prix</p>
|
||||||
|
<p class="text-base-content">{{ component.prix ? `${component.prix} €` : '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-base-content/40 mb-0.5">Fournisseur</p>
|
||||||
|
<div v-if="componentConstructeursDisplay.length">
|
||||||
|
<p
|
||||||
v-for="constructeur in componentConstructeursDisplay"
|
v-for="constructeur in componentConstructeursDisplay"
|
||||||
:key="constructeur.id"
|
:key="constructeur.id"
|
||||||
class="flex flex-col"
|
class="text-base-content"
|
||||||
>
|
|
||||||
<span class="font-medium">{{ constructeur.name }}</span>
|
|
||||||
<span
|
|
||||||
v-if="formatConstructeurContact(constructeur)"
|
|
||||||
class="text-xs text-base-content/50"
|
|
||||||
>
|
>
|
||||||
|
{{ constructeur.name }}
|
||||||
|
<span v-if="formatConstructeurContact(constructeur)" class="text-xs text-base-content/50 block">
|
||||||
{{ formatConstructeurContact(constructeur) }}
|
{{ formatConstructeurContact(constructeur) }}
|
||||||
</span>
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-base-content">—</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="font-medium">Non défini</span>
|
|
||||||
</div>
|
<!-- Product -->
|
||||||
</div>
|
<div v-if="displayProduct" class="rounded-lg border border-base-200 bg-base-100 p-3">
|
||||||
<div class="form-control md:col-span-2">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<label class="label">
|
<div class="space-y-1">
|
||||||
<span class="label-text font-medium">Produit catalogue</span>
|
<p class="text-xs text-base-content/40">Produit catalogue</p>
|
||||||
</label>
|
<p class="text-sm font-semibold text-base-content">{{ displayProductName }}</p>
|
||||||
<div class="input input-bordered input-sm bg-base-200 min-h-[2.75rem] flex flex-col justify-center space-y-1">
|
<p
|
||||||
<template v-if="displayProduct">
|
|
||||||
<span class="font-semibold text-base-content">
|
|
||||||
{{ displayProductName || 'Produit catalogue' }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-for="info in productInfoRows"
|
v-for="info in productInfoRows"
|
||||||
:key="info.label"
|
:key="info.label"
|
||||||
class="text-xs text-base-content/70"
|
class="text-xs text-base-content/60"
|
||||||
>
|
>
|
||||||
{{ info.label }} : {{ info.value }}
|
{{ info.label }} : {{ info.value }}
|
||||||
</span>
|
</p>
|
||||||
|
</div>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="component.product?.id"
|
v-if="component.product?.id"
|
||||||
:to="`/product/${component.product.id}/edit`"
|
:to="`/product/${component.product.id}/edit`"
|
||||||
class="link link-primary text-xs"
|
class="btn btn-ghost btn-xs shrink-0"
|
||||||
>
|
>
|
||||||
Ouvrir la fiche produit
|
Voir le produit
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
|
||||||
<span v-else class="font-medium">Non défini</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<!-- Product documents -->
|
||||||
v-if="productDocuments.length"
|
<div v-if="productDocuments.length" class="mt-3 pt-3 border-t border-base-200 space-y-2">
|
||||||
class="mt-2 space-y-2 rounded-md border border-base-200 bg-base-100 p-3 text-xs"
|
<p class="text-xs font-medium text-base-content/50">Documents du produit</p>
|
||||||
>
|
|
||||||
<h4 class="font-medium text-base-content">
|
|
||||||
Documents du produit
|
|
||||||
</h4>
|
|
||||||
<div
|
<div
|
||||||
v-for="document in productDocuments"
|
v-for="document in productDocuments"
|
||||||
:key="document.id || document.path || document.name"
|
:key="document.id || document.path || document.name"
|
||||||
class="flex items-center justify-between gap-3 rounded border border-base-200 bg-base-100 px-3 py-2"
|
class="flex items-center justify-between gap-3 text-xs"
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
class="flex-shrink-0 overflow-hidden rounded-md border border-base-200 bg-base-200/70 flex items-center justify-center h-12 w-10"
|
|
||||||
>
|
>
|
||||||
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
|
<div class="flex-shrink-0 overflow-hidden rounded border border-base-200 bg-base-200/70 flex items-center justify-center h-8 w-7">
|
||||||
<img
|
<img
|
||||||
v-if="isImageDocument(document) && (document.fileUrl || document.path)"
|
v-if="isImageDocument(document) && (document.fileUrl || document.path)"
|
||||||
:src="document.fileUrl || document.path"
|
:src="document.fileUrl || document.path"
|
||||||
@@ -177,26 +152,18 @@
|
|||||||
<component
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="documentIcon(document).component"
|
:is="documentIcon(document).component"
|
||||||
class="h-6 w-6"
|
class="h-4 w-4"
|
||||||
:class="documentIcon(document).colorClass"
|
:class="documentIcon(document).colorClass"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<span class="truncate text-base-content">{{ document.name }}</span>
|
||||||
<div class="font-medium text-base-content">
|
|
||||||
{{ document.name }}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-base-content/70">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
{{ document.mimeType || 'Inconnu' }} • {{ formatSize(document.size) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 text-xs">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
:disabled="!canPreviewDocument(document)"
|
:disabled="!canPreviewDocument(document)"
|
||||||
:title="canPreviewDocument(document) ? 'Consulter le document' : 'Aucun aperçu disponible pour ce type'"
|
|
||||||
@click="openPreview(document)"
|
@click="openPreview(document)"
|
||||||
>
|
>
|
||||||
Consulter
|
Consulter
|
||||||
@@ -208,11 +175,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<!-- Custom Fields -->
|
||||||
|
|
||||||
<!-- Custom Fields Display - Editable or Read-only -->
|
|
||||||
<CustomFieldDisplay
|
<CustomFieldDisplay
|
||||||
:fields="displayedCustomFields"
|
:fields="displayedCustomFields"
|
||||||
:is-edit-mode="isEditMode"
|
:is-edit-mode="isEditMode"
|
||||||
@@ -220,18 +184,17 @@
|
|||||||
@field-blur="updateComponentCustomField"
|
@field-blur="updateComponentCustomField"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-4 pt-4 border-t border-base-200 space-y-3">
|
<!-- Documents -->
|
||||||
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h4 class="font-semibold text-sm text-base-content/80">
|
<p class="text-xs font-semibold text-base-content/50 uppercase tracking-wide">Documents</p>
|
||||||
Documents
|
<span v-if="isEditMode && selectedFiles.length" class="badge badge-outline badge-xs">
|
||||||
</h4>
|
{{ selectedFiles.length }} fichier{{ selectedFiles.length > 1 ? 's' : '' }}
|
||||||
<span v-if="isEditMode && selectedFiles.length" class="badge badge-outline">
|
|
||||||
{{ selectedFiles.length }} fichier{{ selectedFiles.length > 1 ? 's' : '' }} sélectionné{{ selectedFiles.length > 1 ? 's' : '' }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="loadingDocuments" class="text-xs text-base-content/50">
|
<p v-if="loadingDocuments" class="text-xs text-base-content/50">
|
||||||
Chargement des documents...
|
Chargement...
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<DocumentUpload
|
<DocumentUpload
|
||||||
@@ -254,10 +217,10 @@
|
|||||||
|
|
||||||
<!-- Component Pieces -->
|
<!-- Component Pieces -->
|
||||||
<div v-if="component.pieces && component.pieces.length > 0" class="space-y-2">
|
<div v-if="component.pieces && component.pieces.length > 0" class="space-y-2">
|
||||||
<h4 class="font-semibold text-base-content/80">
|
<p class="text-xs font-semibold text-base-content/50 uppercase tracking-wide">
|
||||||
Pièces du composant
|
Pièces du composant
|
||||||
</h4>
|
</p>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
<div class="space-y-2">
|
||||||
<PieceItem
|
<PieceItem
|
||||||
v-for="piece in component.pieces"
|
v-for="piece in component.pieces"
|
||||||
:key="piece.id"
|
:key="piece.id"
|
||||||
@@ -271,11 +234,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sub Components -->
|
<!-- Sub Components -->
|
||||||
<div v-if="childComponents.length > 0" class="space-y-3">
|
<div v-if="childComponents.length > 0" class="space-y-2">
|
||||||
<h4 class="font-semibold text-base-content/80">
|
<p class="text-xs font-semibold text-base-content/50 uppercase tracking-wide">
|
||||||
Sous-composants
|
Sous-composants
|
||||||
</h4>
|
</p>
|
||||||
<div class="space-y-3 pl-4 border-l-2 border-base-200">
|
<div class="space-y-2 pl-4 border-l-2 border-base-200">
|
||||||
<ComponentItem
|
<ComponentItem
|
||||||
v-for="subComponent in childComponents"
|
v-for="subComponent in childComponents"
|
||||||
:key="subComponent.id"
|
:key="subComponent.id"
|
||||||
@@ -321,11 +284,12 @@ import { useEntityCustomFields } from '~/composables/useEntityCustomFields'
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: { type: Object, required: true },
|
component: { type: Object, required: true },
|
||||||
isEditMode: { type: Boolean, default: false },
|
isEditMode: { type: Boolean, default: false },
|
||||||
|
showDelete: { type: Boolean, default: false },
|
||||||
collapseAll: { type: Boolean, default: true },
|
collapseAll: { type: Boolean, default: true },
|
||||||
toggleToken: { type: Number, default: 0 },
|
toggleToken: { type: Number, default: 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update', 'edit-piece', 'custom-field-update'])
|
const emit = defineEmits(['update', 'edit-piece', 'custom-field-update', 'delete'])
|
||||||
|
|
||||||
// --- Shared composables ---
|
// --- Shared composables ---
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<!-- Piece Header (collapsible, same pattern as ComponentItem) -->
|
<!-- Piece Header (collapsible, same pattern as ComponentItem) -->
|
||||||
<div class="flex items-start justify-between p-4 bg-base-200 rounded-lg">
|
<div class="flex items-start justify-between p-4 bg-base-200 rounded-lg">
|
||||||
<div class="flex items-start gap-3 w-full">
|
<div class="flex items-start gap-3 flex-1 min-w-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-sm btn-circle shrink-0 transition-transform"
|
class="btn btn-ghost btn-sm btn-circle shrink-0 transition-transform"
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<IconLucideChevronRight class="w-5 h-5 transition-transform" aria-hidden="true" />
|
<IconLucideChevronRight class="w-5 h-5 transition-transform" aria-hidden="true" />
|
||||||
<span class="sr-only">{{ isCollapsed ? 'Déplier' : 'Replier' }} la pièce</span>
|
<span class="sr-only">{{ isCollapsed ? 'Déplier' : 'Replier' }} la pièce</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex-1">
|
<div class="flex-1 min-w-0">
|
||||||
<h3 class="text-lg font-semibold">
|
<h3 class="text-lg font-semibold">
|
||||||
{{ pieceData.name }}
|
{{ pieceData.name }}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -49,6 +49,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showDelete"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs text-error shrink-0"
|
||||||
|
title="Supprimer cette pièce"
|
||||||
|
@click="$emit('delete')"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="!isCollapsed" class="space-y-4">
|
<div v-show="!isCollapsed" class="space-y-4">
|
||||||
@@ -250,11 +259,12 @@ import { useEntityCustomFields } from '~/composables/useEntityCustomFields'
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
piece: { type: Object, required: true },
|
piece: { type: Object, required: true },
|
||||||
isEditMode: { type: Boolean, default: false },
|
isEditMode: { type: Boolean, default: false },
|
||||||
|
showDelete: { type: Boolean, default: false },
|
||||||
collapseAll: { type: Boolean, default: true },
|
collapseAll: { type: Boolean, default: true },
|
||||||
toggleToken: { type: Number, default: 0 },
|
toggleToken: { type: Number, default: 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update', 'edit', 'custom-field-update'])
|
const emit = defineEmits(['update', 'edit', 'custom-field-update', 'delete'])
|
||||||
|
|
||||||
// --- Local reactive data for editing ---
|
// --- Local reactive data for editing ---
|
||||||
const pieceData = reactive({
|
const pieceData = reactive({
|
||||||
|
|||||||
@@ -10,6 +10,16 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="menu menu-sm dropdown-content mt-3 z-[1] p-3 shadow-lg bg-base-100 rounded-xl w-60 border border-base-300/50"
|
class="menu menu-sm dropdown-content mt-3 z-[1] p-3 shadow-lg bg-base-100 rounded-xl w-60 border border-base-300/50"
|
||||||
>
|
>
|
||||||
|
<li class="pt-1 pb-2 lg:hidden">
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center gap-2 rounded-lg px-3 py-2 transition-colors text-base-content/70 hover:bg-primary/8 hover:text-primary"
|
||||||
|
@click="toggleDarkMode"
|
||||||
|
>
|
||||||
|
<IconLucideSun v-if="isDark" class="w-4 h-4" aria-hidden="true" />
|
||||||
|
<IconLucideMoon v-else class="w-4 h-4" aria-hidden="true" />
|
||||||
|
{{ isDark ? 'Mode clair' : 'Mode sombre' }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<li class="pt-1 pb-2 lg:hidden">
|
<li class="pt-1 pb-2 lg:hidden">
|
||||||
<button
|
<button
|
||||||
class="w-full flex items-center gap-2 rounded-lg px-3 py-2 transition-colors text-base-content/70 hover:bg-primary/8 hover:text-primary"
|
class="w-full flex items-center gap-2 rounded-lg px-3 py-2 transition-colors text-base-content/70 hover:bg-primary/8 hover:text-primary"
|
||||||
@@ -163,6 +173,14 @@
|
|||||||
<!-- Navbar end -->
|
<!-- Navbar end -->
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-sm btn-circle hidden lg:inline-flex text-base-content/50 hover:text-base-content"
|
||||||
|
:title="isDark ? 'Mode clair' : 'Mode sombre'"
|
||||||
|
@click="toggleDarkMode"
|
||||||
|
>
|
||||||
|
<IconLucideSun v-if="isDark" class="w-4 h-4" aria-hidden="true" />
|
||||||
|
<IconLucideMoon v-else class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-sm btn-circle hidden lg:inline-flex text-base-content/50 hover:text-base-content"
|
class="btn btn-ghost btn-sm btn-circle hidden lg:inline-flex text-base-content/50 hover:text-base-content"
|
||||||
title="Paramètres d'affichage"
|
title="Paramètres d'affichage"
|
||||||
@@ -176,7 +194,7 @@
|
|||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="button"
|
role="button"
|
||||||
class="btn btn-ghost btn-circle btn-sm avatar placeholder indicator"
|
class="indicator cursor-pointer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="unresolvedCount > 0"
|
v-if="unresolvedCount > 0"
|
||||||
@@ -185,9 +203,9 @@
|
|||||||
{{ unresolvedCount }}
|
{{ unresolvedCount }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
class="bg-primary text-primary-content rounded-full w-8 h-8 grid place-items-center"
|
class="bg-primary text-primary-content rounded-full w-8 h-8 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<span class="text-xs font-semibold leading-none tracking-tight">
|
<span class="text-xs font-semibold">
|
||||||
{{ activeProfileInitials }}
|
{{ activeProfileInitials }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -257,6 +275,8 @@ import IconLucideCpu from '~icons/lucide/cpu'
|
|||||||
import IconLucidePuzzle from '~icons/lucide/puzzle'
|
import IconLucidePuzzle from '~icons/lucide/puzzle'
|
||||||
import IconLucidePackage from '~icons/lucide/package'
|
import IconLucidePackage from '~icons/lucide/package'
|
||||||
import IconLucideLink from '~icons/lucide/link'
|
import IconLucideLink from '~icons/lucide/link'
|
||||||
|
import IconLucideSun from '~icons/lucide/sun'
|
||||||
|
import IconLucideMoon from '~icons/lucide/moon'
|
||||||
import logoSrc from '~/assets/LOGO_CARRE_BLANC.png'
|
import logoSrc from '~/assets/LOGO_CARRE_BLANC.png'
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
@@ -334,6 +354,7 @@ const { openDropdown, setDropdown, scheduleDropdownClose, toggleDropdown } = use
|
|||||||
const { activeProfile } = useProfileSession()
|
const { activeProfile } = useProfileSession()
|
||||||
const { isAdmin, canEdit } = usePermissions()
|
const { isAdmin, canEdit } = usePermissions()
|
||||||
const { fetchUnresolvedCount } = useComments()
|
const { fetchUnresolvedCount } = useComments()
|
||||||
|
const { isDark, toggle: toggleDarkMode, init: initDarkMode } = useDarkMode()
|
||||||
|
|
||||||
const unresolvedCount = ref(0)
|
const unresolvedCount = ref(0)
|
||||||
let pollInterval: ReturnType<typeof setInterval> | null = null
|
let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||||
@@ -344,6 +365,7 @@ const refreshUnresolvedCount = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
initDarkMode()
|
||||||
refreshUnresolvedCount()
|
refreshUnresolvedCount()
|
||||||
pollInterval = setInterval(refreshUnresolvedCount, 60_000)
|
pollInterval = setInterval(refreshUnresolvedCount, 60_000)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,22 +25,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-2">
|
<div v-else class="space-y-2">
|
||||||
<div v-for="component in components" :key="component.id" class="relative">
|
<div v-for="component in components" :key="component.id">
|
||||||
<button
|
|
||||||
v-if="isEditMode"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-error btn-xs absolute top-2 right-2 z-10"
|
|
||||||
title="Supprimer ce composant"
|
|
||||||
@click="$emit('remove-component', component.linkId || component.id)"
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
<ComponentHierarchy
|
<ComponentHierarchy
|
||||||
:components="[component]"
|
:components="[component]"
|
||||||
:is-edit-mode="false"
|
:is-edit-mode="false"
|
||||||
|
:show-delete="isEditMode"
|
||||||
:collapse-all="collapsed"
|
:collapse-all="collapsed"
|
||||||
:toggle-token="collapseToggleToken"
|
:toggle-token="collapseToggleToken"
|
||||||
@edit-piece="$emit('edit-piece', $event)"
|
@edit-piece="$emit('edit-piece', $event)"
|
||||||
|
@delete="$emit('remove-component', component.linkId || component.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-error btn-xs btn-square"
|
class="btn btn-ghost btn-xs btn-square text-error"
|
||||||
@click="$emit('remove-field', index)"
|
@click="$emit('remove-field', index)"
|
||||||
>
|
>
|
||||||
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
||||||
|
|||||||
221
app/components/machine/MachineCustomFieldsCard.vue
Normal file
221
app/components/machine/MachineCustomFieldsCard.vue
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card bg-base-100 shadow-lg">
|
||||||
|
<div class="card-body space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="card-title">Champs personnalisés</h2>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
Champs personnalisés propres à cette machine.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span v-if="visibleCustomFields.length" class="badge badge-outline">
|
||||||
|
{{ visibleCustomFields.length }} champ{{ visibleCustomFields.length > 1 ? 's' : '' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- View mode: display values -->
|
||||||
|
<template v-if="!isEditMode">
|
||||||
|
<div v-if="visibleCustomFields.length" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="field in visibleCustomFields"
|
||||||
|
:key="field.customFieldValueId || field.id || field.name"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text text-sm">{{ field.name }}</span>
|
||||||
|
</label>
|
||||||
|
<div class="input input-bordered input-sm bg-base-200">
|
||||||
|
{{ formatCustomFieldValue(field) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-xs text-gray-500">
|
||||||
|
Aucun champ personnalisé défini pour cette machine.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Edit mode: definition management + value editing -->
|
||||||
|
<template v-else>
|
||||||
|
<p v-if="!customFields.length" class="text-xs text-gray-500">
|
||||||
|
Aucun champ personnalisé défini.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(field, index) in customFields"
|
||||||
|
:key="field.id || field.name || index"
|
||||||
|
class="border border-base-200 rounded-md p-3 space-y-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<div class="flex-1 space-y-2">
|
||||||
|
<!-- Definition fields -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||||
|
<input
|
||||||
|
:value="field.name"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm"
|
||||||
|
placeholder="Nom du champ"
|
||||||
|
@blur="handleDefinitionUpdate(field, 'name', ($event.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
:value="field.type || 'text'"
|
||||||
|
class="select select-bordered select-sm"
|
||||||
|
@change="handleDefinitionUpdate(field, 'type', ($event.target as HTMLSelectElement).value)"
|
||||||
|
>
|
||||||
|
<option value="text">Texte</option>
|
||||||
|
<option value="number">Nombre</option>
|
||||||
|
<option value="select">Liste</option>
|
||||||
|
<option value="boolean">Oui/Non</option>
|
||||||
|
<option value="date">Date</option>
|
||||||
|
</select>
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
:checked="!!field.required"
|
||||||
|
@change="handleDefinitionUpdate(field, 'required', ($event.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
Obligatoire
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Options for select type -->
|
||||||
|
<textarea
|
||||||
|
v-if="(field.type || 'text') === 'select'"
|
||||||
|
:value="field.optionsText || (Array.isArray(field.options) ? field.options.join('\n') : '')"
|
||||||
|
class="textarea textarea-bordered textarea-sm h-20 w-full"
|
||||||
|
placeholder="Option 1 Option 2"
|
||||||
|
@blur="handleOptionsUpdate(field, ($event.target as HTMLTextAreaElement).value)"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<!-- Value editing -->
|
||||||
|
<div class="pt-1 border-t border-base-200">
|
||||||
|
<label class="label py-0">
|
||||||
|
<span class="label-text text-xs text-base-content/60">Valeur</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-if="!field.type || field.type === 'text'"
|
||||||
|
:value="field.value ?? ''"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm w-full"
|
||||||
|
placeholder="Valeur..."
|
||||||
|
@input="$emit('set-custom-field-value', field, ($event.target as HTMLInputElement).value)"
|
||||||
|
@blur="$emit('update-custom-field', field)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else-if="field.type === 'number'"
|
||||||
|
:value="field.value ?? ''"
|
||||||
|
type="number"
|
||||||
|
class="input input-bordered input-sm w-full"
|
||||||
|
placeholder="Valeur..."
|
||||||
|
@input="$emit('set-custom-field-value', field, ($event.target as HTMLInputElement).value)"
|
||||||
|
@blur="$emit('update-custom-field', field)"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
v-else-if="field.type === 'select'"
|
||||||
|
:value="field.value ?? ''"
|
||||||
|
class="select select-bordered select-sm w-full"
|
||||||
|
@change="onSelectChange(field, ($event.target as HTMLSelectElement).value)"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner...</option>
|
||||||
|
<option
|
||||||
|
v-for="option in field.options"
|
||||||
|
:key="option"
|
||||||
|
:value="option"
|
||||||
|
>
|
||||||
|
{{ option }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
v-else-if="field.type === 'boolean'"
|
||||||
|
class="flex items-center gap-3 cursor-pointer py-1"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-primary toggle-sm"
|
||||||
|
:checked="String(field.value).toLowerCase() === 'true'"
|
||||||
|
@change="onBooleanChange(field, ($event.target as HTMLInputElement).checked)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-sm"
|
||||||
|
:class="String(field.value).toLowerCase() === 'true' ? 'text-success font-medium' : 'text-base-content/60'"
|
||||||
|
>
|
||||||
|
{{ String(field.value).toLowerCase() === 'true' ? 'Oui' : 'Non' }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-else-if="field.type === 'date'"
|
||||||
|
:value="field.value ?? ''"
|
||||||
|
type="date"
|
||||||
|
class="input input-bordered input-sm w-full"
|
||||||
|
@input="$emit('set-custom-field-value', field, ($event.target as HTMLInputElement).value)"
|
||||||
|
@blur="$emit('update-custom-field', field)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-sm btn-square flex-shrink-0 text-error"
|
||||||
|
title="Supprimer ce champ"
|
||||||
|
@click="$emit('delete-field', field.id || field.customFieldId)"
|
||||||
|
>
|
||||||
|
<IconLucideTrash class="w-4 h-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm md:btn-md btn-primary"
|
||||||
|
@click="$emit('add-field')"
|
||||||
|
>
|
||||||
|
Ajouter un champ personnalisé
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconLucideTrash from '~icons/lucide/trash'
|
||||||
|
import { formatCustomFieldValue } from '~/shared/utils/customFieldUtils'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
customFields: any[]
|
||||||
|
visibleCustomFields: any[]
|
||||||
|
isEditMode: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'set-custom-field-value': [field: any, value: unknown]
|
||||||
|
'update-custom-field': [field: any]
|
||||||
|
'add-field': []
|
||||||
|
'delete-field': [fieldId: string]
|
||||||
|
'update-field-definition': [fieldId: string, data: Record<string, unknown>]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleDefinitionUpdate = (field: any, key: string, value: unknown) => {
|
||||||
|
const fieldId = field.id || field.customFieldId
|
||||||
|
if (!fieldId) return
|
||||||
|
emit('update-field-definition', fieldId, { ...field, [key]: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOptionsUpdate = (field: any, raw: string) => {
|
||||||
|
const fieldId = field.id || field.customFieldId
|
||||||
|
if (!fieldId) return
|
||||||
|
const options = raw.split('\n').map((o: string) => o.trim()).filter((o: string) => o.length > 0)
|
||||||
|
emit('update-field-definition', fieldId, { ...field, options })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelectChange = (field: any, value: string) => {
|
||||||
|
emit('set-custom-field-value', field, value)
|
||||||
|
emit('update-custom-field', field)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBooleanChange = (field: any, checked: boolean) => {
|
||||||
|
emit('set-custom-field-value', field, checked ? 'true' : 'false')
|
||||||
|
emit('update-custom-field', field)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="isEditMode"
|
v-if="isEditMode"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-error btn-xs"
|
class="btn btn-ghost btn-xs text-error"
|
||||||
:disabled="uploading"
|
:disabled="uploading"
|
||||||
@click="$emit('remove', doc.id)"
|
@click="$emit('remove', doc.id)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,6 +20,29 @@
|
|||||||
{{ machineName }}
|
{{ machineName }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Site</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
v-if="isEditMode"
|
||||||
|
:value="machineSiteId"
|
||||||
|
class="select select-bordered"
|
||||||
|
@change="$emit('update:machine-site-id', ($event.target as HTMLSelectElement).value); $emit('blur-field')"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un site</option>
|
||||||
|
<option
|
||||||
|
v-for="site in sites"
|
||||||
|
:key="site.id"
|
||||||
|
:value="site.id"
|
||||||
|
>
|
||||||
|
{{ site.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div v-else class="input input-bordered bg-base-200">
|
||||||
|
{{ machineSiteName || 'Non défini' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="isEditMode || machineReference" class="form-control">
|
<div v-if="isEditMode || machineReference" class="form-control">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Référence</span>
|
<span class="label-text">Référence</span>
|
||||||
@@ -184,6 +207,9 @@ const props = defineProps<{
|
|||||||
isEditMode: boolean
|
isEditMode: boolean
|
||||||
machineName: string
|
machineName: string
|
||||||
machineReference: string
|
machineReference: string
|
||||||
|
machineSiteId: string
|
||||||
|
machineSiteName: string
|
||||||
|
sites: any[]
|
||||||
machineConstructeurIds: string[]
|
machineConstructeurIds: string[]
|
||||||
machineConstructeursDisplay: any[]
|
machineConstructeursDisplay: any[]
|
||||||
hasMachineConstructeur: boolean
|
hasMachineConstructeur: boolean
|
||||||
@@ -196,6 +222,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:machine-name': [value: string]
|
'update:machine-name': [value: string]
|
||||||
'update:machine-reference': [value: string]
|
'update:machine-reference': [value: string]
|
||||||
|
'update:machine-site-id': [value: string]
|
||||||
'update:constructeur-ids': [ids: unknown]
|
'update:constructeur-ids': [ids: unknown]
|
||||||
'blur-field': []
|
'blur-field': []
|
||||||
'set-custom-field-value': [field: any, value: unknown]
|
'set-custom-field-value': [field: any, value: unknown]
|
||||||
|
|||||||
@@ -25,22 +25,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-2">
|
<div v-else class="space-y-2">
|
||||||
<div v-for="piece in pieces" :key="piece.id" class="relative">
|
<div v-for="piece in pieces" :key="piece.id">
|
||||||
<button
|
|
||||||
v-if="isEditMode"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-error btn-xs absolute top-2 right-2 z-10"
|
|
||||||
title="Supprimer cette pièce"
|
|
||||||
@click="$emit('remove-piece', piece.linkId || piece.id)"
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
<PieceItem
|
<PieceItem
|
||||||
:piece="piece"
|
:piece="piece"
|
||||||
:is-edit-mode="false"
|
:is-edit-mode="false"
|
||||||
|
:show-delete="isEditMode"
|
||||||
:collapse-all="collapsed"
|
:collapse-all="collapsed"
|
||||||
:toggle-token="collapseToggleToken"
|
:toggle-token="collapseToggleToken"
|
||||||
@edit="$emit('edit-piece', $event)"
|
@edit="$emit('edit-piece', $event)"
|
||||||
|
@delete="$emit('remove-piece', piece.linkId || piece.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,24 +24,26 @@
|
|||||||
<div
|
<div
|
||||||
v-for="product in products"
|
v-for="product in products"
|
||||||
:key="product.id || product.name"
|
:key="product.id || product.name"
|
||||||
class="rounded border border-base-200 bg-base-200/60 p-3 text-sm space-y-2 relative"
|
class="rounded border border-base-200 bg-base-200/60 p-3 text-sm space-y-2"
|
||||||
>
|
>
|
||||||
|
<div class="flex items-center justify-between flex-wrap gap-2">
|
||||||
|
<p class="font-semibold text-base-content">
|
||||||
|
{{ product.name }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span v-if="product.groupLabel" class="badge badge-ghost badge-sm">
|
||||||
|
{{ product.groupLabel }}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
v-if="isEditMode"
|
v-if="isEditMode"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-error btn-xs absolute top-2 right-2"
|
class="btn btn-ghost btn-xs text-error"
|
||||||
title="Supprimer ce produit"
|
title="Supprimer ce produit"
|
||||||
@click="$emit('remove-product', (product.linkId || product.id) as string)"
|
@click="$emit('remove-product', (product.linkId || product.id) as string)"
|
||||||
>
|
>
|
||||||
Supprimer
|
Supprimer
|
||||||
</button>
|
</button>
|
||||||
<div class="flex items-center justify-between flex-wrap gap-2">
|
</div>
|
||||||
<p class="font-semibold text-base-content">
|
|
||||||
{{ product.name }}
|
|
||||||
</p>
|
|
||||||
<span v-if="product.groupLabel" class="badge badge-ghost badge-sm">
|
|
||||||
{{ product.groupLabel }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p v-if="product.reference" class="text-xs text-base-content/70">
|
<p v-if="product.reference" class="text-xs text-base-content/70">
|
||||||
<span class="font-medium">Référence :</span>
|
<span class="font-medium">Référence :</span>
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow">
|
<div
|
||||||
|
class="card site-card shadow-md hover:shadow-xl transition-shadow overflow-hidden"
|
||||||
|
:style="{
|
||||||
|
borderTop: site.color ? `4px solid ${site.color}` : '4px solid transparent',
|
||||||
|
background: site.color ? `linear-gradient(160deg, ${site.color}30 0%, ${site.color}08 40%, var(--color-base-100) 100%)` : undefined,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h3 class="card-title text-lg">
|
<h3 class="card-title text-lg text-base-content">
|
||||||
{{ site.name }}
|
{{ site.name }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="badge badge-primary badge-sm">
|
<div
|
||||||
|
class="badge font-bold"
|
||||||
|
:style="site.color ? { backgroundColor: site.color + '30', color: site.color, borderColor: site.color + '50' } : {}"
|
||||||
|
:class="!site.color ? 'badge-primary' : ''"
|
||||||
|
>
|
||||||
{{ machineCount }} machines
|
{{ machineCount }} machines
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,46 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Couleur</span>
|
||||||
|
</label>
|
||||||
|
<div v-if="siteRef.color" class="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
:value="siteRef.color"
|
||||||
|
type="color"
|
||||||
|
class="w-10 h-10 rounded cursor-pointer border border-base-300"
|
||||||
|
:disabled="disabled"
|
||||||
|
@input="(e: Event) => { siteRef.color = (e.target as HTMLInputElement).value }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="siteRef.color"
|
||||||
|
type="text"
|
||||||
|
placeholder="#000000"
|
||||||
|
class="input input-bordered input-sm flex-1"
|
||||||
|
:disabled="disabled"
|
||||||
|
maxlength="7"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="siteRef.color = ''"
|
||||||
|
>
|
||||||
|
Effacer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline btn-sm w-fit"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="siteRef.color = '#3b82f6'"
|
||||||
|
>
|
||||||
|
Choisir une couleur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SiteContactFormFields :form="siteRef" :disabled="disabled" />
|
<SiteContactFormFields :form="siteRef" :disabled="disabled" />
|
||||||
|
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
@@ -39,6 +79,7 @@ import SiteContactFormFields from '~/components/sites/SiteContactFormFields.vue'
|
|||||||
|
|
||||||
type SiteForm = {
|
type SiteForm = {
|
||||||
name: string
|
name: string
|
||||||
|
color: string
|
||||||
contactName: string
|
contactName: string
|
||||||
contactPhone: string
|
contactPhone: string
|
||||||
contactAddress: string
|
contactAddress: string
|
||||||
|
|||||||
@@ -20,6 +20,46 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Couleur</span>
|
||||||
|
</label>
|
||||||
|
<div v-if="form.color" class="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
:value="form.color"
|
||||||
|
type="color"
|
||||||
|
class="w-10 h-10 rounded cursor-pointer border border-base-300"
|
||||||
|
:disabled="disabled"
|
||||||
|
@input="form.color = $event.target.value"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="form.color"
|
||||||
|
type="text"
|
||||||
|
placeholder="#000000"
|
||||||
|
class="input input-bordered input-sm flex-1"
|
||||||
|
:disabled="disabled"
|
||||||
|
maxlength="7"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="form.color = ''"
|
||||||
|
>
|
||||||
|
Effacer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline btn-sm w-fit"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="form.color = '#3b82f6'"
|
||||||
|
>
|
||||||
|
Choisir une couleur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SiteContactFormFields :form="props.form" :disabled="disabled" />
|
<SiteContactFormFields :form="props.form" :disabled="disabled" />
|
||||||
|
|
||||||
<div class="border-t border-base-200 pt-4 space-y-4">
|
<div class="border-t border-base-200 pt-4 space-y-4">
|
||||||
|
|||||||
26
app/composables/useDarkMode.ts
Normal file
26
app/composables/useDarkMode.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const isDark = ref(false)
|
||||||
|
|
||||||
|
export function useDarkMode() {
|
||||||
|
const toggle = () => {
|
||||||
|
isDark.value = !isDark.value
|
||||||
|
applyTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyTheme = () => {
|
||||||
|
const theme = isDark.value ? 'mytheme-dark' : 'mytheme'
|
||||||
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
|
localStorage.setItem('theme', theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
const saved = localStorage.getItem('theme')
|
||||||
|
if (saved === 'mytheme-dark') {
|
||||||
|
isDark.value = true
|
||||||
|
} else if (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
isDark.value = true
|
||||||
|
}
|
||||||
|
applyTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isDark, toggle, init }
|
||||||
|
}
|
||||||
@@ -15,6 +15,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 { useConstructeurs } from '~/composables/useConstructeurs'
|
import { useConstructeurs } from '~/composables/useConstructeurs'
|
||||||
|
import { useSites } from '~/composables/useSites'
|
||||||
import { useMachinePrint } from '~/composables/useMachinePrint'
|
import { useMachinePrint } from '~/composables/useMachinePrint'
|
||||||
import {
|
import {
|
||||||
resolveConstructeurs,
|
resolveConstructeurs,
|
||||||
@@ -44,6 +45,7 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
const { get } = useApi()
|
const { get } = useApi()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { constructeurs, loadConstructeurs } = useConstructeurs()
|
const { constructeurs, loadConstructeurs } = useConstructeurs()
|
||||||
|
const { sites, loadSites } = useSites()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
printModalOpen,
|
printModalOpen,
|
||||||
@@ -64,6 +66,7 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
// Machine fields
|
// Machine fields
|
||||||
const machineName = ref('')
|
const machineName = ref('')
|
||||||
const machineReference = ref('')
|
const machineReference = ref('')
|
||||||
|
const machineSiteId = ref('')
|
||||||
const machineConstructeurIds = ref<string[]>([])
|
const machineConstructeurIds = ref<string[]>([])
|
||||||
|
|
||||||
const machineConstructeurId = computed({
|
const machineConstructeurId = computed({
|
||||||
@@ -229,6 +232,7 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
machine.value.constructeurs,
|
machine.value.constructeurs,
|
||||||
machine.value.constructeur,
|
machine.value.constructeur,
|
||||||
)
|
)
|
||||||
|
machineSiteId.value = (machine.value.siteId as string) || (machine.value.site as AnyRecord)?.id as string || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +259,7 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
machine,
|
machine,
|
||||||
machineName,
|
machineName,
|
||||||
machineReference,
|
machineReference,
|
||||||
|
machineSiteId,
|
||||||
machineConstructeurIds,
|
machineConstructeurIds,
|
||||||
machineDocumentsLoaded,
|
machineDocumentsLoaded,
|
||||||
machineComponentLinks,
|
machineComponentLinks,
|
||||||
@@ -401,6 +406,7 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
loadConstructeurs(),
|
loadConstructeurs(),
|
||||||
loadComponentTypes(),
|
loadComponentTypes(),
|
||||||
loadPieceTypes(),
|
loadPieceTypes(),
|
||||||
|
loadSites(),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,8 +425,9 @@ export function useMachineDetailData(machineId: string) {
|
|||||||
machineComponentLinks, machinePieceLinks, machineProductLinks,
|
machineComponentLinks, machinePieceLinks, machineProductLinks,
|
||||||
|
|
||||||
// Machine fields
|
// Machine fields
|
||||||
machineName, machineReference, machineConstructeurIds, machineConstructeurId,
|
machineName, machineReference, machineSiteId, machineConstructeurIds, machineConstructeurId,
|
||||||
machineConstructeursDisplay, machineConstructeurContact, hasMachineConstructeur,
|
machineConstructeursDisplay, machineConstructeurContact, hasMachineConstructeur,
|
||||||
|
sites,
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
machineDocumentFiles, machineDocumentsUploading, machineDocumentsLoaded,
|
machineDocumentFiles, machineDocumentsUploading, machineDocumentsLoaded,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface UseMachineDetailUpdatesDeps {
|
|||||||
machine: Ref<AnyRecord | null>
|
machine: Ref<AnyRecord | null>
|
||||||
machineName: Ref<string>
|
machineName: Ref<string>
|
||||||
machineReference: Ref<string>
|
machineReference: Ref<string>
|
||||||
|
machineSiteId: Ref<string>
|
||||||
machineConstructeurIds: Ref<string[]>
|
machineConstructeurIds: Ref<string[]>
|
||||||
machineDocumentsLoaded: Ref<boolean>
|
machineDocumentsLoaded: Ref<boolean>
|
||||||
machineComponentLinks: Ref<AnyRecord[]>
|
machineComponentLinks: Ref<AnyRecord[]>
|
||||||
@@ -40,6 +41,7 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
machine,
|
machine,
|
||||||
machineName,
|
machineName,
|
||||||
machineReference,
|
machineReference,
|
||||||
|
machineSiteId,
|
||||||
machineConstructeurIds,
|
machineConstructeurIds,
|
||||||
machineComponentLinks,
|
machineComponentLinks,
|
||||||
machinePieceLinks,
|
machinePieceLinks,
|
||||||
@@ -63,6 +65,7 @@ export function useMachineDetailUpdates(deps: UseMachineDetailUpdatesDeps) {
|
|||||||
const result: any = await updateMachineApi(machine.value.id as string, {
|
const result: any = await updateMachineApi(machine.value.id as string, {
|
||||||
name: machineName.value,
|
name: machineName.value,
|
||||||
reference: machineReference.value,
|
reference: machineReference.value,
|
||||||
|
siteId: machineSiteId.value || undefined,
|
||||||
constructeurIds: cIds,
|
constructeurIds: cIds,
|
||||||
} as any)
|
} as any)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { canPreviewDocument } from '~/utils/documentPreview'
|
|||||||
|
|
||||||
type SiteForm = {
|
type SiteForm = {
|
||||||
name: string
|
name: string
|
||||||
|
color: string
|
||||||
contactName: string
|
contactName: string
|
||||||
contactPhone: string
|
contactPhone: string
|
||||||
contactAddress: string
|
contactAddress: string
|
||||||
@@ -31,6 +32,7 @@ type SiteDocument = {
|
|||||||
type SiteWithDocuments = {
|
type SiteWithDocuments = {
|
||||||
id: string
|
id: string
|
||||||
name?: string
|
name?: string
|
||||||
|
color?: string
|
||||||
contactName?: string
|
contactName?: string
|
||||||
contactPhone?: string
|
contactPhone?: string
|
||||||
contactAddress?: string
|
contactAddress?: string
|
||||||
@@ -54,6 +56,7 @@ export function useSiteManagement() {
|
|||||||
|
|
||||||
const newSite = reactive<SiteForm>({
|
const newSite = reactive<SiteForm>({
|
||||||
name: '',
|
name: '',
|
||||||
|
color: '',
|
||||||
contactName: '',
|
contactName: '',
|
||||||
contactPhone: '',
|
contactPhone: '',
|
||||||
contactAddress: '',
|
contactAddress: '',
|
||||||
@@ -63,6 +66,7 @@ export function useSiteManagement() {
|
|||||||
|
|
||||||
const editSiteForm = reactive<SiteForm>({
|
const editSiteForm = reactive<SiteForm>({
|
||||||
name: '',
|
name: '',
|
||||||
|
color: '',
|
||||||
contactName: '',
|
contactName: '',
|
||||||
contactPhone: '',
|
contactPhone: '',
|
||||||
contactAddress: '',
|
contactAddress: '',
|
||||||
@@ -81,6 +85,7 @@ export function useSiteManagement() {
|
|||||||
|
|
||||||
const resetNewSite = () => {
|
const resetNewSite = () => {
|
||||||
newSite.name = ''
|
newSite.name = ''
|
||||||
|
newSite.color = ''
|
||||||
newSite.contactName = ''
|
newSite.contactName = ''
|
||||||
newSite.contactPhone = ''
|
newSite.contactPhone = ''
|
||||||
newSite.contactAddress = ''
|
newSite.contactAddress = ''
|
||||||
@@ -101,6 +106,7 @@ export function useSiteManagement() {
|
|||||||
const handleCreateSite = async () => {
|
const handleCreateSite = async () => {
|
||||||
const result = await createSite({
|
const result = await createSite({
|
||||||
name: newSite.name,
|
name: newSite.name,
|
||||||
|
color: newSite.color,
|
||||||
contactName: newSite.contactName,
|
contactName: newSite.contactName,
|
||||||
contactPhone: newSite.contactPhone,
|
contactPhone: newSite.contactPhone,
|
||||||
contactAddress: newSite.contactAddress,
|
contactAddress: newSite.contactAddress,
|
||||||
@@ -116,6 +122,7 @@ export function useSiteManagement() {
|
|||||||
const editSite = (site: SiteWithDocuments) => {
|
const editSite = (site: SiteWithDocuments) => {
|
||||||
siteBeingEdited.value = site
|
siteBeingEdited.value = site
|
||||||
editSiteForm.name = site.name || ''
|
editSiteForm.name = site.name || ''
|
||||||
|
editSiteForm.color = site.color || ''
|
||||||
editSiteForm.contactName = site.contactName || ''
|
editSiteForm.contactName = site.contactName || ''
|
||||||
editSiteForm.contactPhone = site.contactPhone || ''
|
editSiteForm.contactPhone = site.contactPhone || ''
|
||||||
editSiteForm.contactAddress = site.contactAddress || ''
|
editSiteForm.contactAddress = site.contactAddress || ''
|
||||||
@@ -148,6 +155,7 @@ export function useSiteManagement() {
|
|||||||
|
|
||||||
const baseUpdate = {
|
const baseUpdate = {
|
||||||
name: editSiteForm.name,
|
name: editSiteForm.name,
|
||||||
|
color: editSiteForm.color,
|
||||||
contactName: editSiteForm.contactName,
|
contactName: editSiteForm.contactName,
|
||||||
contactPhone: editSiteForm.contactPhone,
|
contactPhone: editSiteForm.contactPhone,
|
||||||
contactAddress: editSiteForm.contactAddress,
|
contactAddress: editSiteForm.contactAddress,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { extractCollection } from '~/shared/utils/apiHelpers'
|
|||||||
export interface Site {
|
export interface Site {
|
||||||
id: string
|
id: string
|
||||||
name?: string
|
name?: string
|
||||||
|
color?: string
|
||||||
contactName?: string
|
contactName?: string
|
||||||
contactPhone?: string
|
contactPhone?: string
|
||||||
contactAddress?: string
|
contactAddress?: string
|
||||||
|
|||||||
@@ -69,6 +69,22 @@ const badgeClass = (type: ChangeType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const releases: Release[] = [
|
const releases: Release[] = [
|
||||||
|
{
|
||||||
|
version: 'v1.9.0',
|
||||||
|
date: '2026-03-09',
|
||||||
|
changes: [
|
||||||
|
{ type: 'feat', text: 'Gestion des champs personnalisés sur les machines : ajout, modification et suppression de définitions de champs directement depuis la fiche machine' },
|
||||||
|
{ type: 'feat', text: 'Refonte UI globale : amélioration du styling, des layouts et du responsive sur l\'ensemble des composants et pages' },
|
||||||
|
{ type: 'feat', text: 'Suite de tests API complète : 167 tests couvrant toutes les entités, la sécurité et les validations' },
|
||||||
|
{ type: 'feat', text: 'Endpoint /api/health pour le monitoring applicatif' },
|
||||||
|
{ type: 'fix', text: 'Sécurité renforcée : désactivation de la migration de session sur le firewall API, durcissement des accès documents et sessions' },
|
||||||
|
{ type: 'fix', text: 'Confirmation de suppression avec impact sur le catalogue produits (documents, liaisons machines en cascade)' },
|
||||||
|
{ type: 'fix', text: 'Correction du débordement des dropdowns dans les DataTable' },
|
||||||
|
{ type: 'perf', text: 'Refactoring massif du frontend : extraction de 15+ composables et composants partagés, réduction de la taille des fichiers' },
|
||||||
|
{ type: 'chore', text: 'Extraction de CuidEntityTrait et abstraction du subscriber d\'audit côté backend' },
|
||||||
|
{ type: 'chore', text: 'Ajout de DAMA DoctrineTestBundle pour l\'isolation des tests par transaction' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: 'v1.8.1',
|
version: 'v1.8.1',
|
||||||
date: '2026-03-05',
|
date: '2026-03-05',
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell-actions="{ row }">
|
<template #cell-actions="{ row }">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="`/component/${row.component.id}/edit`"
|
:to="`/component/${row.component.id}/edit`"
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="canEdit"
|
v-if="canEdit"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-error btn-xs"
|
class="btn btn-ghost btn-xs text-error"
|
||||||
:disabled="loadingComposants"
|
:disabled="loadingComposants"
|
||||||
@click="handleDeleteComponent(row.component)"
|
@click="handleDeleteComponent(row.component)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -49,11 +49,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell-actions="{ row }">
|
<template #cell-actions="{ row }">
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<button class="btn btn-ghost btn-xs" @click="openEditModal(row)">
|
<button class="btn btn-ghost btn-xs" @click="openEditModal(row)">
|
||||||
{{ canEdit ? 'Modifier' : 'Consulter' }}
|
{{ canEdit ? 'Modifier' : 'Consulter' }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="canEdit" class="btn btn-error btn-xs" @click="confirmDelete(row)">
|
<button v-if="canEdit" class="btn btn-ghost btn-xs text-error" @click="confirmDelete(row)">
|
||||||
Supprimer
|
Supprimer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="card bg-base-100 shadow-sm mb-8">
|
<div class="card bg-base-100 shadow-sm mb-8">
|
||||||
<div class="card-body py-4">
|
<div class="card-body py-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="flex flex-col md:flex-row md:items-end gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control flex-1">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text text-xs font-semibold uppercase tracking-wide text-base-content/50">Rechercher</span>
|
<span class="label-text text-xs font-semibold uppercase tracking-wide text-base-content/50">Rechercher</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -41,11 +41,11 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control md:w-64">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text text-xs font-semibold uppercase tracking-wide text-base-content/50">Site</span>
|
<span class="label-text text-xs font-semibold uppercase tracking-wide text-base-content/50">Site</span>
|
||||||
</label>
|
</label>
|
||||||
<select v-model="selectedSiteFilter" class="select select-bordered">
|
<select v-model="selectedSiteFilter" class="select select-bordered w-full">
|
||||||
<option value="">
|
<option value="">
|
||||||
Tous les sites
|
Tous les sites
|
||||||
</option>
|
</option>
|
||||||
@@ -102,19 +102,26 @@
|
|||||||
<div
|
<div
|
||||||
v-for="site in filteredSites"
|
v-for="site in filteredSites"
|
||||||
:key="site.id"
|
:key="site.id"
|
||||||
class="card bg-base-100 shadow-sm overflow-hidden"
|
class="card site-card shadow-md overflow-hidden"
|
||||||
|
:style="{
|
||||||
|
borderTop: site.color ? `4px solid ${site.color}` : '4px solid transparent',
|
||||||
|
background: site.color ? `linear-gradient(160deg, ${site.color}30 0%, ${site.color}08 40%, var(--color-base-100) 100%)` : undefined,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- Site Header -->
|
<!-- Site Header -->
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
class="w-11 h-11 rounded-xl bg-primary/10 grid place-items-center shrink-0"
|
class="w-11 h-11 rounded-xl grid place-items-center shrink-0"
|
||||||
|
:style="{ backgroundColor: site.color ? site.color + '40' : 'oklch(var(--p) / 0.1)', border: site.color ? `2px solid ${site.color}60` : 'none' }"
|
||||||
>
|
>
|
||||||
<IconLucideMapPin class="w-5 h-5 text-primary" aria-hidden="true" />
|
<IconLucideMapPin class="w-5 h-5" :style="{ color: site.color || 'oklch(var(--p))' }" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h3 class="text-lg font-bold text-base-content tracking-tight">
|
<h3
|
||||||
|
class="text-lg font-bold tracking-tight text-base-content"
|
||||||
|
>
|
||||||
{{ site.name }}
|
{{ site.name }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-1.5 text-sm text-base-content/50">
|
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-1.5 text-sm text-base-content/50">
|
||||||
@@ -134,7 +141,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<span class="badge badge-primary badge-sm font-semibold">
|
<span
|
||||||
|
class="badge font-bold"
|
||||||
|
:style="site.color ? { backgroundColor: site.color + '30', color: site.color, borderColor: site.color + '50' } : {}"
|
||||||
|
:class="!site.color ? 'badge-primary' : ''"
|
||||||
|
>
|
||||||
{{ site.machines?.length || 0 }}
|
{{ site.machines?.length || 0 }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -163,7 +174,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="machine in site.machines"
|
v-for="machine in site.machines"
|
||||||
:key="machine.id"
|
:key="machine.id"
|
||||||
class="group rounded-xl border border-base-300/40 bg-base-200/30 hover:bg-base-200/60 hover:border-primary/20 transition-all cursor-pointer p-4"
|
class="group flex flex-col rounded-xl border border-base-300/40 bg-base-100 shadow-sm hover:shadow-md hover:border-primary/20 transition-all cursor-pointer p-4"
|
||||||
@click="viewMachineDetails(machine)"
|
@click="viewMachineDetails(machine)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
@@ -172,29 +183,29 @@
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="machine.reference" class="flex items-center gap-1.5 text-xs text-base-content/40 mb-3">
|
<div v-if="machine.reference" class="flex items-center gap-1.5 text-xs text-base-content/40">
|
||||||
<IconLucideTag class="w-3 h-3" aria-hidden="true" />
|
<IconLucideTag class="w-3 h-3" aria-hidden="true" />
|
||||||
<span>{{ machine.reference }}</span>
|
<span>{{ machine.reference }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-1.5 justify-end">
|
<div class="mt-auto pt-3 flex items-center justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
v-if="canEdit"
|
v-if="canEdit"
|
||||||
class="btn btn-xs btn-ghost text-base-content/40 hover:text-base-content opacity-0 group-hover:opacity-100 transition-opacity"
|
class="btn btn-ghost btn-sm"
|
||||||
@click.stop="editMachine(machine)"
|
@click.stop="editMachine(machine)"
|
||||||
>
|
>
|
||||||
Modifier
|
Modifier
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canEdit"
|
v-if="canEdit"
|
||||||
class="btn btn-xs btn-ghost text-error/50 hover:text-error opacity-0 group-hover:opacity-100 transition-opacity"
|
class="btn btn-ghost btn-sm text-error"
|
||||||
@click.stop="confirmDeleteMachine(machine)"
|
@click.stop="confirmDeleteMachine(machine)"
|
||||||
>
|
>
|
||||||
Supprimer
|
Supprimer
|
||||||
</button>
|
</button>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="`/machine/${machine.id}`"
|
:to="`/machine/${machine.id}`"
|
||||||
class="btn btn-xs btn-primary btn-outline opacity-0 group-hover:opacity-100 transition-opacity"
|
class="btn btn-primary btn-sm"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
Détails
|
Détails
|
||||||
|
|||||||
@@ -39,7 +39,11 @@
|
|||||||
rounded
|
rounded
|
||||||
>
|
>
|
||||||
<div class="flex justify-center gap-4">
|
<div class="flex justify-center gap-4">
|
||||||
<div v-if="d.machine.value.site?.name" class="badge badge-outline">
|
<div
|
||||||
|
v-if="d.machine.value.site?.name"
|
||||||
|
class="badge badge-outline font-semibold"
|
||||||
|
:style="d.machine.value.site?.color ? { borderColor: d.machine.value.site.color + '60', backgroundColor: d.machine.value.site.color + '25', color: d.machine.value.site.color } : {}"
|
||||||
|
>
|
||||||
{{ d.machine.value.site?.name }}
|
{{ d.machine.value.site?.name }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="d.machine.value.reference" class="badge badge-outline">
|
<div v-if="d.machine.value.reference" class="badge badge-outline">
|
||||||
@@ -53,6 +57,9 @@
|
|||||||
:is-edit-mode="d.isEditMode.value"
|
:is-edit-mode="d.isEditMode.value"
|
||||||
:machine-name="d.machineName.value"
|
:machine-name="d.machineName.value"
|
||||||
:machine-reference="d.machineReference.value"
|
:machine-reference="d.machineReference.value"
|
||||||
|
:machine-site-id="d.machineSiteId.value"
|
||||||
|
:machine-site-name="d.machine.value?.site?.name ?? ''"
|
||||||
|
:sites="d.sites.value"
|
||||||
:machine-constructeur-ids="d.machineConstructeurIds.value"
|
:machine-constructeur-ids="d.machineConstructeurIds.value"
|
||||||
:machine-constructeurs-display="d.machineConstructeursDisplay.value"
|
:machine-constructeurs-display="d.machineConstructeursDisplay.value"
|
||||||
:has-machine-constructeur="d.hasMachineConstructeur.value"
|
:has-machine-constructeur="d.hasMachineConstructeur.value"
|
||||||
@@ -62,6 +69,7 @@
|
|||||||
:machine-custom-field-defs="d.machine.value?.customFields ?? []"
|
:machine-custom-field-defs="d.machine.value?.customFields ?? []"
|
||||||
@update:machine-name="d.machineName.value = $event"
|
@update:machine-name="d.machineName.value = $event"
|
||||||
@update:machine-reference="d.machineReference.value = $event"
|
@update:machine-reference="d.machineReference.value = $event"
|
||||||
|
@update:machine-site-id="d.machineSiteId.value = $event"
|
||||||
@update:constructeur-ids="d.handleMachineConstructeurChange"
|
@update:constructeur-ids="d.handleMachineConstructeurChange"
|
||||||
@blur-field="d.updateMachineInfo"
|
@blur-field="d.updateMachineInfo"
|
||||||
@set-custom-field-value="d.setMachineCustomFieldValue"
|
@set-custom-field-value="d.setMachineCustomFieldValue"
|
||||||
|
|||||||
@@ -65,10 +65,14 @@
|
|||||||
<div
|
<div
|
||||||
v-for="machine in filteredMachines"
|
v-for="machine in filteredMachines"
|
||||||
:key="machine.id"
|
:key="machine.id"
|
||||||
class="card bg-base-100 shadow-sm hover:shadow-xl transition-shadow cursor-pointer"
|
class="card site-card shadow-md hover:shadow-xl transition-shadow cursor-pointer overflow-hidden"
|
||||||
|
:style="{
|
||||||
|
borderTop: machine.site?.color ? `4px solid ${machine.site.color}` : '4px solid transparent',
|
||||||
|
background: machine.site?.color ? `linear-gradient(160deg, ${machine.site.color}30 0%, ${machine.site.color}08 40%, var(--color-base-100) 100%)` : undefined,
|
||||||
|
}"
|
||||||
@click="viewMachineDetails(machine)"
|
@click="viewMachineDetails(machine)"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body flex flex-col">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<h3 class="card-title text-lg">
|
<h3 class="card-title text-lg">
|
||||||
{{ machine.name }}
|
{{ machine.name }}
|
||||||
@@ -77,8 +81,11 @@
|
|||||||
|
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<IconLucideMapPin class="w-4 h-4 text-blue-500" aria-hidden="true" />
|
<IconLucideMapPin class="w-4 h-4" :style="{ color: machine.site?.color || '#3b82f6' }" aria-hidden="true" />
|
||||||
<span class="text-gray-600">{{ machine.site?.name || 'Site inconnu' }}</span>
|
<span
|
||||||
|
class="font-bold text-sm px-2.5 py-1 rounded-lg text-base-content"
|
||||||
|
:style="machine.site?.color ? { backgroundColor: machine.site.color + '30', border: `1px solid ${machine.site.color}40` } : {}"
|
||||||
|
>{{ machine.site?.name || 'Site inconnu' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="machine.reference" class="flex items-center gap-2">
|
<div v-if="machine.reference" class="flex items-center gap-2">
|
||||||
@@ -87,15 +94,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions justify-end mt-4">
|
<div class="mt-auto pt-3 flex items-center justify-end gap-2">
|
||||||
<button class="btn btn-sm btn-outline" @click.stop="editMachine(machine)">
|
<button v-if="canEdit" class="btn btn-ghost btn-sm" @click.stop="editMachine(machine)">
|
||||||
Modifier
|
Modifier
|
||||||
</button>
|
</button>
|
||||||
<button v-if="canEdit" class="btn btn-sm btn-error" @click.stop="confirmDeleteMachine(machine)">
|
<button v-if="canEdit" class="btn btn-ghost btn-sm text-error" @click.stop="confirmDeleteMachine(machine)">
|
||||||
Supprimer
|
Supprimer
|
||||||
</button>
|
</button>
|
||||||
<NuxtLink :to="`/machine/${machine.id}`" class="btn btn-sm btn-primary">
|
<NuxtLink :to="`/machine/${machine.id}`" class="btn btn-primary btn-sm">
|
||||||
Voir détails
|
Détails
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell-actions="{ row }">
|
<template #cell-actions="{ row }">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="`/pieces/${row.piece.id}/edit`"
|
:to="`/pieces/${row.piece.id}/edit`"
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="canEdit"
|
v-if="canEdit"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-error btn-xs"
|
class="btn btn-ghost btn-xs text-error"
|
||||||
:disabled="loadingPieces"
|
:disabled="loadingPieces"
|
||||||
@click="handleDeletePiece(row.piece)"
|
@click="handleDeletePiece(row.piece)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell-actions="{ row }">
|
<template #cell-actions="{ row }">
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
:to="`/product/${row.product.id}/edit`"
|
:to="`/product/${row.product.id}/edit`"
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
|
|||||||
Reference in New Issue
Block a user