Compare commits

...

3 Commits

Author SHA1 Message Date
Matthieu
27d51ffdb1 fix(toasts) : auto-dismiss des notifications d'erreur apres 8 secondes
Les toasts d'erreur etaient persistants (duration force a 0) et restaient
affiches jusqu'a fermeture manuelle, ce qui pouvait empiler des messages
obsoletes a l'ecran. Aligne le comportement sur les autres types : duree
par defaut 8s (plus que warning a 6s pour laisser le temps de lire). Une
erreur critique peut toujours etre rendue persistante en passant
explicitement showError(msg, 0).
2026-05-06 16:51:08 +02:00
Matthieu
53d4d5768b refactor(doc) : utilise palier comme exemple plus parlant que pompe
Remplace l'exemple "pompe avec position sur la machine" par un palier
de tete vs palier de pied : exemple plus concret et plus universellement
compris pour illustrer la difference entre champs catalogue et champs
contextuels (custom field values).
2026-05-06 16:40:37 +02:00
Matthieu
3ff89d43ed fix(db) : ajoute les FK CASCADE manquantes documents.composantId et machine_component_links.composantId
Les entités Doctrine déclaraient déjà onDelete: CASCADE pour ces deux
relations, mais les contraintes correspondantes étaient absentes en base.
Conséquence : la suppression d'un composant pouvait laisser des documents
ou des links machine orphelins. La migration nettoie les orphelins
existants (avec trace dans audit_logs) puis ajoute les deux FK.
2026-05-06 16:34:26 +02:00
3 changed files with 127 additions and 20 deletions

View File

@@ -33,7 +33,7 @@ export function useToast() {
message,
type,
visible: true,
duration: type === 'error' ? 0 : duration,
duration,
}
if (toasts.value.length >= MAX_TOASTS) {
@@ -42,8 +42,7 @@ export function useToast() {
toasts.value.push(toast)
// Only auto-dismiss non-error toasts
if (type !== 'error' && duration > 0) {
if (duration > 0) {
setTimeout(() => {
removeToast(id)
}, duration)
@@ -56,8 +55,8 @@ export function useToast() {
return showToast(message, 'success', duration)
}
const showError = (message: string): number => {
return showToast(message, 'error', 0)
const showError = (message: string, duration = 8000): number => {
return showToast(message, 'error', duration)
}
const showWarning = (message: string, duration = 6000): number => {

View File

@@ -715,10 +715,12 @@
</p>
<p class="text-base-content/70 leading-relaxed mb-4">
<strong>Pourquoi ?</strong> Certaines informations n'ont de sens que quand
l'element est monte sur une machine. Par exemple, la "position sur la machine"
d'une pompe : dans le catalogue, la pompe n'est montee nulle part, donc ce champ
ne sert a rien. Mais quand on regarde cette pompe depuis la fiche d'une machine,
on veut savoir ou elle est installee.
l'element est monte sur une machine. Prenons l'exemple d'un palier : sur une
machine, vous en avez souvent deux, un en haut (le palier de tete) et un en
bas (le palier de pied). Dans le catalogue, le palier n'est monte nulle part,
donc savoir s'il est "en haut" ou "en bas" ne veut rien dire. Mais des qu'on
regarde ce palier depuis la fiche d'une machine, on veut savoir lequel des
deux c'est.
</p>
</div>
</div>
@@ -731,16 +733,16 @@
<p class="text-xs text-base-content/40 mb-4">Quand on consulte l'element tout seul</p>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-base-content/70">Debit max</span>
<span>120 L/min</span>
<span class="text-base-content/70">Diametre interieur</span>
<span>50 mm</span>
</div>
<div class="flex justify-between">
<span class="text-base-content/70">ATEX</span>
<span>Oui</span>
<span class="text-base-content/70">Type</span>
<span>Roulement a billes</span>
</div>
<div class="border-t border-dashed border-base-300 pt-2 mt-2">
<div class="flex justify-between opacity-30">
<span class="line-through">Position sur la machine</span>
<span class="line-through">Emplacement</span>
<span class="text-xs italic">pas affiche ici</span>
</div>
</div>
@@ -753,17 +755,17 @@
<p class="text-xs text-base-content/40 mb-4">Quand on regarde l'element dans sa machine</p>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-base-content/70">Debit max</span>
<span>120 L/min</span>
<span class="text-base-content/70">Diametre interieur</span>
<span>50 mm</span>
</div>
<div class="flex justify-between">
<span class="text-base-content/70">ATEX</span>
<span>Oui</span>
<span class="text-base-content/70">Type</span>
<span>Roulement a billes</span>
</div>
<div class="border-t border-primary/20 pt-2 mt-2">
<div class="flex justify-between bg-primary/10 rounded px-2 py-1.5">
<span class="text-base-content font-medium">Position sur la machine</span>
<span class="font-bold">Secteur B - Ligne 3</span>
<span class="text-base-content font-medium">Emplacement</span>
<span class="font-bold">Haut (palier de tete)</span>
</div>
</div>
</div>

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260506140000_FixComposantCascadeFKs extends AbstractMigration
{
public function getDescription(): string
{
return 'Add missing CASCADE FKs documents.composantid and machine_component_links.composantid; cleanup pre-existing orphan rows';
}
public function up(Schema $schema): void
{
// 1. Trace des suppressions à venir dans audit_logs (actor = NULL = "system").
// On copie un snapshot minimal avant DELETE pour cohérence avec les autres "delete".
$this->addSql(<<<'SQL'
INSERT INTO audit_logs (id, entitytype, entityid, action, snapshot, actorprofileid, createdat)
SELECT
'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24),
'document',
d.id,
'delete',
json_build_object(
'id', d.id,
'name', d.name,
'filename', d.filename,
'composantId', d.composantid,
'note', 'Cleaned by FK cascade fix migration (Version20260506140000) - referenced composant no longer existed'
),
NULL,
NOW()
FROM documents d
WHERE d.composantid IS NOT NULL
AND d.composantid NOT IN (SELECT id FROM composants)
SQL);
$this->addSql(<<<'SQL'
INSERT INTO audit_logs (id, entitytype, entityid, action, snapshot, actorprofileid, createdat)
SELECT
'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24),
'machine_component_link',
l.id,
'delete',
json_build_object(
'id', l.id,
'machineId', l.machineid,
'composantId', l.composantid,
'note', 'Cleaned by FK cascade fix migration (Version20260506140000) - referenced composant no longer existed'
),
NULL,
NOW()
FROM machine_component_links l
WHERE l.composantid IS NOT NULL
AND l.composantid NOT IN (SELECT id FROM composants)
SQL);
// 2. Nettoyage des orphelins.
$this->addSql(<<<'SQL'
DELETE FROM documents
WHERE composantid IS NOT NULL
AND composantid NOT IN (SELECT id FROM composants)
SQL);
$this->addSql(<<<'SQL'
DELETE FROM machine_component_links
WHERE composantid IS NOT NULL
AND composantid NOT IN (SELECT id FROM composants)
SQL);
// 3. Ajout idempotent des 2 FK manquantes (alignement avec les entités Doctrine).
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'fk_documents_composant' AND table_name = 'documents'
) THEN
ALTER TABLE documents ADD CONSTRAINT fk_documents_composant
FOREIGN KEY (composantid) REFERENCES composants(id) ON DELETE CASCADE;
END IF;
END $$;
SQL);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'fk_mcl_composant' AND table_name = 'machine_component_links'
) THEN
ALTER TABLE machine_component_links ADD CONSTRAINT fk_mcl_composant
FOREIGN KEY (composantid) REFERENCES composants(id) ON DELETE CASCADE;
END IF;
END $$;
SQL);
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS fk_documents_composant');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT IF EXISTS fk_mcl_composant');
}
}