Replace demo data with 2026 R&D planning, fix zoom and dimension-aware creation
Seeder: Replace 12 demo projects with 6 real 2026 projects from Planning PPTX: - BRIDGE (Pilot Klundert), CRISP (Compressor Aanbesteding), WISE (Monsternamekast), Gemaal 3.0, Afvlakkingsregeling, Structuur & Borging - 4 strategic themes: Architectuur, Productiewaardig, Lab, Governance - Real team members, commitments, documents, and dependencies MetroCanvas: Fix zoom-out scaling - Wider transition range (0.6→0.25 instead of 0.5→0.1) for smoother feel - Animated zoom reset on dimension commit (400ms ease) instead of jarring snap - Guard against re-entry during transitions with isCommitting flag - Expose dimension metadata (parentEntityType/Id/Name) for parent components FloatingActions: Dimension-aware creation - Shows "Nieuw commitment/document" when inside a project dimension - Shows "Nieuw project/thema" at root level - Receives depth and parentEntityType props from MetroMap MetroMap: Wire dimension tracking - Tracks canvasDepth/canvasDimension from MetroCanvas dimension-change events - Updates breadcrumb for both page-level and canvas-level navigation - Passes dimension context to FloatingActions and CommitmentForm MapDataService: Add parent metadata to buildProjectChildren output Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -197,7 +197,7 @@ class MapDataService
|
||||
'order' => $i + 1,
|
||||
'status' => $fase->status->value,
|
||||
'badge' => ucfirst($fase->status->value),
|
||||
'children' => null, // Phases could have children in future
|
||||
'children' => null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ class MapDataService
|
||||
'status' => $commitment->status->value,
|
||||
'owner' => $commitment->eigenaar?->name,
|
||||
'badge' => $commitment->deadline?->format('d M Y'),
|
||||
'children' => null, // Could contain acties in future
|
||||
'children' => null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -240,6 +240,10 @@ class MapDataService
|
||||
'lines' => $lines,
|
||||
'nodes' => $nodes,
|
||||
'connections' => [],
|
||||
// Metadata so the frontend knows what dimension it's in
|
||||
'parentEntityType' => 'project',
|
||||
'parentEntityId' => $project->id,
|
||||
'parentName' => $project->naam,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*
|
||||
* Planning 2026 – R&D Lab Waterschap Brabantse Delta
|
||||
* Source: Planning 2026.pptx
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
@@ -56,483 +59,442 @@ class DatabaseSeeder extends Seeder
|
||||
]);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 2. Users
|
||||
// 2. Users (R&D team)
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
$adminUser = User::create([
|
||||
'name' => 'Admin Gebruiker',
|
||||
$admin = User::create([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@innovatieplatform.nl',
|
||||
'password' => Hash::make('password'),
|
||||
'functie' => 'Platform Beheerder',
|
||||
'afdeling' => 'R&D Lab',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
$adminUser->roles()->attach($roleAdmin);
|
||||
$admin->roles()->attach($roleAdmin);
|
||||
|
||||
$testUser = User::create([
|
||||
$rene = User::create([
|
||||
'name' => 'Rene de Ren',
|
||||
'email' => 'rene@wbd-rd.nl',
|
||||
'password' => Hash::make('password'),
|
||||
'functie' => 'R&D Engineer',
|
||||
'functie' => 'R&D Engineer / Teamlead',
|
||||
'afdeling' => 'R&D Lab',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
$testUser->roles()->attach($roleProjectOwner);
|
||||
$rene->roles()->attach($roleProjectOwner);
|
||||
|
||||
$analyst = User::create([
|
||||
'name' => 'Lisanne Bakker',
|
||||
'email' => 'l.bakker@wbd-rd.nl',
|
||||
$pim = User::create([
|
||||
'name' => 'Pim Moerman',
|
||||
'email' => 'p.moerman@wbd-rd.nl',
|
||||
'password' => Hash::make('password'),
|
||||
'functie' => 'Data Analist',
|
||||
'afdeling' => 'Watermanagement',
|
||||
'functie' => 'Technisch Adviseur OT/IT',
|
||||
'afdeling' => 'R&D Lab',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
$analyst->roles()->attach($roleTeamMember);
|
||||
$pim->roles()->attach($roleTeamMember);
|
||||
|
||||
$engineer = User::create([
|
||||
'name' => 'Joris van Dam',
|
||||
'email' => 'j.vandam@wbd-rd.nl',
|
||||
$sjoerd = User::create([
|
||||
'name' => 'Sjoerd Fijnje',
|
||||
'email' => 's.fijnje@wbd-rd.nl',
|
||||
'password' => Hash::make('password'),
|
||||
'functie' => 'Senior Technisch Adviseur',
|
||||
'afdeling' => 'Infrastructuur',
|
||||
'functie' => 'Werktuigbouwkundige / Elektro',
|
||||
'afdeling' => 'R&D Lab',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
$engineer->roles()->attach($roleTeamMember);
|
||||
$sjoerd->roles()->attach($roleTeamMember);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 3. Themas (4 strategic themes)
|
||||
// 3. Themas – 2026 strategic themes
|
||||
//
|
||||
// Kaders: geen nieuwe ML-modellen, geen nieuwe digital twins,
|
||||
// focus op werkend krijgen, architectuur aantonen, overdragen.
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
$themaWater = Thema::create([
|
||||
'naam' => 'Waterkwaliteit',
|
||||
'beschrijving' => 'Verbetering van de kwaliteit van oppervlaktewater en grondwater door innovatieve monitoring- en zuiveringstechnieken.',
|
||||
$themaArchitectuur = Thema::create([
|
||||
'naam' => 'Architectuur & Veiligheid',
|
||||
'beschrijving' => 'Aantonen dat de EDGE-laag architectuur veilig, betrouwbaar en schaalbaar is. OT/IT-scheiding, Siemens-koppeling, CI/CD implementatie.',
|
||||
'prioriteit' => Prioriteit::Hoog,
|
||||
'periode_start' => '2025-01-01',
|
||||
'periode_eind' => '2028-12-31',
|
||||
'periode_start' => '2026-01-01',
|
||||
'periode_eind' => '2026-12-31',
|
||||
]);
|
||||
|
||||
$themaInfra = Thema::create([
|
||||
'naam' => 'Slimme Infrastructuur',
|
||||
'beschrijving' => 'Digitalisering en automatisering van waterkeringen, gemalen en sluizen voor efficiënter beheer en snellere responstijden.',
|
||||
$themaProductie = Thema::create([
|
||||
'naam' => 'Productiewaardig Maken',
|
||||
'beschrijving' => 'Bestaande innovaties werkend, overdraagbaar en schaalbaar maken. Begeleiding van aanbestedingen en overdracht naar Bouwen/Beheer.',
|
||||
'prioriteit' => Prioriteit::Hoog,
|
||||
'periode_start' => '2025-01-01',
|
||||
'periode_eind' => '2028-12-31',
|
||||
'periode_start' => '2026-01-01',
|
||||
'periode_eind' => '2026-12-31',
|
||||
]);
|
||||
|
||||
$themaData = Thema::create([
|
||||
'naam' => 'Data-gedreven Beheer',
|
||||
'beschrijving' => 'Inzet van data-analyse, AI en digitale tweelingen voor betere besluitvorming in waterbeheer.',
|
||||
$themaLab = Thema::create([
|
||||
'naam' => 'Lab & Prototyping',
|
||||
'beschrijving' => 'Fysieke opstellingen valideren, uitbreiden en koppelen tot een realistische testomgeving voor afvlakkingsregelingen.',
|
||||
'prioriteit' => Prioriteit::Midden,
|
||||
'periode_start' => '2025-06-01',
|
||||
'periode_eind' => '2029-06-30',
|
||||
'periode_start' => '2026-01-01',
|
||||
'periode_eind' => '2026-12-31',
|
||||
]);
|
||||
|
||||
$themaDuurzaam = Thema::create([
|
||||
'naam' => 'Duurzaamheid & Klimaatadaptatie',
|
||||
'beschrijving' => 'Innovaties gericht op energieneutraliteit, circulaire waterketens en klimaatrobuuste inrichting van het beheergebied.',
|
||||
$themaGovernance = Thema::create([
|
||||
'naam' => 'Governance & Teamborging',
|
||||
'beschrijving' => 'R&D minder persoonsafhankelijk maken. Projectstructuur, documentatie, besluitvorming en rolafbakening vastleggen.',
|
||||
'prioriteit' => Prioriteit::Midden,
|
||||
'periode_start' => '2025-01-01',
|
||||
'periode_eind' => '2030-12-31',
|
||||
'periode_start' => '2026-01-01',
|
||||
'periode_eind' => '2026-12-31',
|
||||
]);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 4. Speerpunten (2 per thema)
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Waterkwaliteit
|
||||
$spWaterMonitoring = Speerpunt::create([
|
||||
'thema_id' => $themaWater->id,
|
||||
'naam' => 'Real-time Watermonitoring',
|
||||
'beschrijving' => 'Continue meting van waterkwaliteitsparameters via IoT-sensoren in het beheergebied.',
|
||||
'eigenaar_id' => $testUser->id,
|
||||
// Architectuur & Veiligheid
|
||||
$spEdge = Speerpunt::create([
|
||||
'thema_id' => $themaArchitectuur->id,
|
||||
'naam' => 'EDGE-laag & OT/IT-scheiding',
|
||||
'beschrijving' => 'Implementatie en validatie van de EDGE-architectuur met beveiligde OT/IT-scheiding op pilotlocatie.',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'status' => SpeerpuntStatus::Actief,
|
||||
]);
|
||||
|
||||
$spMicropollution = Speerpunt::create([
|
||||
'thema_id' => $themaWater->id,
|
||||
'naam' => 'Microverontreinigingen',
|
||||
'beschrijving' => 'Detectie en verwijdering van opkomende stoffen zoals medicijnresten en PFAS.',
|
||||
'eigenaar_id' => $engineer->id,
|
||||
$spCicd = Speerpunt::create([
|
||||
'thema_id' => $themaArchitectuur->id,
|
||||
'naam' => 'CI/CD & DevOps R&D-stack',
|
||||
'beschrijving' => 'Continuous Integration en Deployment implementeren in de R&D-stack voor maximale efficiëntie en herhaalbaarheid.',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'status' => SpeerpuntStatus::Concept,
|
||||
]);
|
||||
|
||||
// Slimme Infrastructuur
|
||||
$spDigitaalBeheer = Speerpunt::create([
|
||||
'thema_id' => $themaInfra->id,
|
||||
'naam' => 'Digitaal Kunstwerkenregister',
|
||||
'beschrijving' => 'Volledig digitaal beheer van kunstwerken met BIM-koppeling en conditiebewaking.',
|
||||
'eigenaar_id' => $engineer->id,
|
||||
// Productiewaardig Maken
|
||||
$spOverdracht = Speerpunt::create([
|
||||
'thema_id' => $themaProductie->id,
|
||||
'naam' => 'Overdracht naar Bouwen/Beheer',
|
||||
'beschrijving' => 'Innovaties begeleiden van R&D naar productie: documentatie, oplevering, kennisoverdracht.',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'status' => SpeerpuntStatus::Actief,
|
||||
]);
|
||||
|
||||
$spSmartGemaal = Speerpunt::create([
|
||||
'thema_id' => $themaInfra->id,
|
||||
'naam' => 'Slimme Gemaalbesturing',
|
||||
'beschrijving' => 'Predictieve sturing van gemalen op basis van weersvoorspelling en waterstanden.',
|
||||
'eigenaar_id' => $testUser->id,
|
||||
$spAanbesteding = Speerpunt::create([
|
||||
'thema_id' => $themaProductie->id,
|
||||
'naam' => 'Aanbesteding & TCO-toetsing',
|
||||
'beschrijving' => 'Technische toetsing van aanbestedingen op prestatie-eisen, energie-efficiëntie en inpasbaarheid.',
|
||||
'eigenaar_id' => $pim->id,
|
||||
'status' => SpeerpuntStatus::Actief,
|
||||
]);
|
||||
|
||||
// Data-gedreven Beheer
|
||||
$spDigitaalTwin = Speerpunt::create([
|
||||
'thema_id' => $themaData->id,
|
||||
'naam' => 'Digitale Tweeling Watersysteem',
|
||||
'beschrijving' => 'Virtueel model van het watersysteem voor scenario-analyse en operationele ondersteuning.',
|
||||
'eigenaar_id' => $analyst->id,
|
||||
// Lab & Prototyping
|
||||
$spDtValidatie = Speerpunt::create([
|
||||
'thema_id' => $themaLab->id,
|
||||
'naam' => 'Digital Twin Validatie',
|
||||
'beschrijving' => 'Valideren van 2025-ontwikkelingen (Measurement, Rotating Machine, Aeration Tank, MGC, Diffuser) op het fysieke prototype.',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'status' => SpeerpuntStatus::Actief,
|
||||
]);
|
||||
|
||||
$spGemalenketen = Speerpunt::create([
|
||||
'thema_id' => $themaLab->id,
|
||||
'naam' => 'Gemalenketen Testomgeving',
|
||||
'beschrijving' => 'Realiseren van een volledige gemalenketen in het lab voor experimentele afvlakkingsregelingen.',
|
||||
'eigenaar_id' => $sjoerd->id,
|
||||
'status' => SpeerpuntStatus::Concept,
|
||||
]);
|
||||
|
||||
$spAIVoorspelling = Speerpunt::create([
|
||||
'thema_id' => $themaData->id,
|
||||
'naam' => 'AI-gestuurde Waterstandsvoorspelling',
|
||||
'beschrijving' => 'Machine learning modellen voor nauwkeurige korte- en middellangetermijnwaterstanden.',
|
||||
'eigenaar_id' => $analyst->id,
|
||||
// Governance & Teamborging
|
||||
$spProjectstructuur = Speerpunt::create([
|
||||
'thema_id' => $themaGovernance->id,
|
||||
'naam' => 'Projectstructuur & Documentatie',
|
||||
'beschrijving' => 'Afspraken vastleggen over projectstructuur, documentatie, besluitvorming en beleid.',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'status' => SpeerpuntStatus::Actief,
|
||||
]);
|
||||
|
||||
// Duurzaamheid
|
||||
$spEnergieneutraal = Speerpunt::create([
|
||||
'thema_id' => $themaDuurzaam->id,
|
||||
'naam' => 'Energieneutrale Zuivering',
|
||||
'beschrijving' => 'Zelfvoorzienende rwzi\'s door terugwinning van energie uit afvalwater.',
|
||||
'eigenaar_id' => $engineer->id,
|
||||
'status' => SpeerpuntStatus::Actief,
|
||||
]);
|
||||
|
||||
$spCirculaireWater = Speerpunt::create([
|
||||
'thema_id' => $themaDuurzaam->id,
|
||||
'naam' => 'Circulaire Waterketen',
|
||||
'beschrijving' => 'Terugwinning van grondstoffen (fosfaat, cellulose, warmte) uit afvalwater.',
|
||||
'eigenaar_id' => $testUser->id,
|
||||
$spRolafbakening = Speerpunt::create([
|
||||
'thema_id' => $themaGovernance->id,
|
||||
'naam' => 'Rolafbakening R&D ↔ Organisatie',
|
||||
'beschrijving' => 'Verduidelijken wat R&D wel en niet doet. Afbakening richting Beheer, Bouwen, ICT en Datalab.',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'status' => SpeerpuntStatus::Concept,
|
||||
]);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 5. Projects (10–12 spread across themes and lifecycle phases)
|
||||
// 5. Projects – 2026 planning (6 projects from the presentation)
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
$projects = [];
|
||||
|
||||
// --- Waterkwaliteit projects ---
|
||||
$p1 = $this->createProject([
|
||||
'speerpunt_id' => $spWaterMonitoring->id,
|
||||
'naam' => 'LoRaWAN Sensornetwerk Biesbosch',
|
||||
'beschrijving' => 'Uitrol van een draadloos sensornetwerk in het Biesbosch-gebied voor real-time meting van waterkwaliteitsparameters (pH, DO, troebelheid, geleidbaarheid).',
|
||||
'eigenaar_id' => $testUser->id,
|
||||
// --- B.R.I.D.G.E (Bidirectional Real-time Interface for Data & Grid Exchange) ---
|
||||
$bridge = $this->createProject([
|
||||
'speerpunt_id' => $spEdge->id,
|
||||
'naam' => 'B.R.I.D.G.E – Pilot Klundert',
|
||||
'beschrijving' => 'Aantonen dat de nieuwe EDGE-laag veilig en betrouwbaar assets kan uitlezen en aansturen, en correct kan koppelen met Siemens-omgevingen. Implementatie Ubuntu LTS, Node-RED orkestratie, OPC UA communicatie met Siemens PLC (T-serie).',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'status' => ProjectStatus::Pilot,
|
||||
'prioriteit' => Prioriteit::Hoog,
|
||||
'startdatum' => '2025-03-01',
|
||||
'streef_einddatum' => '2025-12-31',
|
||||
'startdatum' => '2026-01-15',
|
||||
'streef_einddatum' => '2026-09-30',
|
||||
], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment, FaseType::Pilot]);
|
||||
|
||||
$p2 = $this->createProject([
|
||||
'speerpunt_id' => $spWaterMonitoring->id,
|
||||
'naam' => 'Drone-inspectie Waterbodem',
|
||||
'beschrijving' => 'Onderzoek naar de inzet van autonome onderwaterdrones voor sedimentkartering en vervuilingdetectie in kanalen en sloten.',
|
||||
'eigenaar_id' => $engineer->id,
|
||||
'status' => ProjectStatus::Experiment,
|
||||
'prioriteit' => Prioriteit::Midden,
|
||||
'startdatum' => '2025-06-01',
|
||||
'streef_einddatum' => '2026-06-30',
|
||||
], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment]);
|
||||
|
||||
$p3 = $this->createProject([
|
||||
'speerpunt_id' => $spMicropollution->id,
|
||||
'naam' => 'PFAS Detectiemethode Ontwikkeling',
|
||||
'beschrijving' => 'Ontwikkeling van een snelle en goedkope veldmethode voor PFAS-detectie in oppervlaktewater, als alternatief voor kostbare laboratoriumanalyse.',
|
||||
'eigenaar_id' => $analyst->id,
|
||||
// --- C.R.I.S.P (Compressor Replacement for Immediate System Performance) ---
|
||||
$crisp = $this->createProject([
|
||||
'speerpunt_id' => $spAanbesteding->id,
|
||||
'naam' => 'C.R.I.S.P – Compressor Aanbesteding',
|
||||
'beschrijving' => 'Technische toetsing van aanbestedingsmethodiek voor compressorvervanging Nieuwveer. Prestatie-eisen, energie-efficiëntie, inpasbaarheid in procesautomatisering. R&D als inhoudelijke sparringpartner voor Bouwen.',
|
||||
'eigenaar_id' => $pim->id,
|
||||
'status' => ProjectStatus::Verkenning,
|
||||
'prioriteit' => Prioriteit::Hoog,
|
||||
'startdatum' => '2025-09-01',
|
||||
'streef_einddatum' => '2026-09-30',
|
||||
'startdatum' => '2026-02-01',
|
||||
'streef_einddatum' => '2026-08-31',
|
||||
], [FaseType::Signaal, FaseType::Verkenning]);
|
||||
|
||||
// --- Slimme Infrastructuur projects ---
|
||||
$p4 = $this->createProject([
|
||||
'speerpunt_id' => $spDigitaalBeheer->id,
|
||||
'naam' => 'BIM-model Gemaal De Donge',
|
||||
'beschrijving' => 'Digitale driedimensionale representatie van gemaal De Donge inclusief alle technische installaties, leidingen en elektrotechnische componenten.',
|
||||
'eigenaar_id' => $engineer->id,
|
||||
// --- W.I.S.E (Weather and Influent Sampling Engine) ---
|
||||
$wise = $this->createProject([
|
||||
'speerpunt_id' => $spOverdracht->id,
|
||||
'naam' => 'W.I.S.E – Monsternamekast Overdracht',
|
||||
'beschrijving' => 'Overdracht monsternamekast naar Bouwen. Opleveren Node-RED flows, documentatie, afbakening R&D vs beheer. Samenwerking met Datalab (data & integratie voorspellend model) en Beheer/Operatie.',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'status' => ProjectStatus::OverdrachtBouwen,
|
||||
'prioriteit' => Prioriteit::Midden,
|
||||
'startdatum' => '2024-09-01',
|
||||
'streef_einddatum' => '2025-06-30',
|
||||
'prioriteit' => Prioriteit::Hoog,
|
||||
'startdatum' => '2025-06-01',
|
||||
'streef_einddatum' => '2026-06-30',
|
||||
], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment, FaseType::Pilot, FaseType::Besluitvorming, FaseType::OverdrachtBouwen]);
|
||||
|
||||
$p5 = $this->createProject([
|
||||
'speerpunt_id' => $spSmartGemaal->id,
|
||||
'naam' => 'Predictieve Gemaalbesturing Mark-Vliet',
|
||||
'beschrijving' => 'Implementatie van een ML-algoritme dat op basis van KNMI-weerdata en historische afvoerpatronen de optimale pompsturing berekent voor het Mark-Vliet systeem.',
|
||||
'eigenaar_id' => $testUser->id,
|
||||
'status' => ProjectStatus::Besluitvorming,
|
||||
'prioriteit' => Prioriteit::Hoog,
|
||||
'startdatum' => '2025-01-01',
|
||||
'streef_einddatum' => '2025-12-31',
|
||||
], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment, FaseType::Pilot, FaseType::Besluitvorming]);
|
||||
// --- Gemaal 3.0 – Prototype Validatie ---
|
||||
$gemaal = $this->createProject([
|
||||
'speerpunt_id' => $spDtValidatie->id,
|
||||
'naam' => 'Gemaal 3.0 – Prototype Validatie',
|
||||
'beschrijving' => 'Valideren dat het recent gebouwde prototype functioneel correct is. Testen van alle elektrische aansluitingen, I/O-functionaliteit en randapparatuur. Go/no-go voor verdere uitrol naar 2 kopieën.',
|
||||
'eigenaar_id' => $sjoerd->id,
|
||||
'status' => ProjectStatus::Experiment,
|
||||
'prioriteit' => Prioriteit::Midden,
|
||||
'startdatum' => '2026-03-01',
|
||||
'streef_einddatum' => '2026-07-31',
|
||||
], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment]);
|
||||
|
||||
$p6 = $this->createProject([
|
||||
'speerpunt_id' => $spSmartGemaal->id,
|
||||
'naam' => 'Remote Monitoring Waterkeringen',
|
||||
'beschrijving' => 'Continuemonitoring van primaire waterkeringen met IoT-sensoren voor zakking, piping-detectie en grondwaterstand.',
|
||||
'eigenaar_id' => $engineer->id,
|
||||
'status' => ProjectStatus::Signaal,
|
||||
'prioriteit' => Prioriteit::Laag,
|
||||
'startdatum' => '2026-01-01',
|
||||
'streef_einddatum' => null,
|
||||
], [FaseType::Signaal]);
|
||||
|
||||
// --- Data-gedreven Beheer projects ---
|
||||
$p7 = $this->createProject([
|
||||
'speerpunt_id' => $spDigitaalTwin->id,
|
||||
'naam' => 'Digitale Tweeling Pilot Roosendaalse Vliet',
|
||||
'beschrijving' => 'Eerste proof-of-concept van een digitale tweeling voor het deelgebied Roosendaalse Vliet, gekoppeld aan het SOBEK-hydraulisch model.',
|
||||
'eigenaar_id' => $analyst->id,
|
||||
// --- Afvlakkingsregeling – Lab Gemalenketen ---
|
||||
$afvlak = $this->createProject([
|
||||
'speerpunt_id' => $spGemalenketen->id,
|
||||
'naam' => 'Afvlakkingsregeling – Lab Keten',
|
||||
'beschrijving' => 'Nabootsen van een volledige gemalenketen in het lab, klaarstomen voor afvlakkingsregeling. Realiseren 2 extra opstellingen, koppelen tot keten, basissoftware testen op ketengedrag.',
|
||||
'eigenaar_id' => $sjoerd->id,
|
||||
'status' => ProjectStatus::Concept,
|
||||
'prioriteit' => Prioriteit::Hoog,
|
||||
'startdatum' => '2025-07-01',
|
||||
'prioriteit' => Prioriteit::Midden,
|
||||
'startdatum' => '2026-06-01',
|
||||
'streef_einddatum' => '2026-12-31',
|
||||
], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept]);
|
||||
|
||||
$p8 = $this->createProject([
|
||||
'speerpunt_id' => $spAIVoorspelling->id,
|
||||
'naam' => 'AI Waterstandsmodel Hollandsch Diep',
|
||||
'beschrijving' => 'Training en validatie van een LSTM-neuraal netwerk voor 48-uurs waterstandsvoorspellingen op het Hollandsch Diep, ter vervanging van het huidige regressiemodel.',
|
||||
'eigenaar_id' => $analyst->id,
|
||||
'status' => ProjectStatus::Experiment,
|
||||
'prioriteit' => Prioriteit::Hoog,
|
||||
'startdatum' => '2025-04-01',
|
||||
'streef_einddatum' => '2026-03-31',
|
||||
], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment]);
|
||||
|
||||
$p9 = $this->createProject([
|
||||
'speerpunt_id' => $spAIVoorspelling->id,
|
||||
'naam' => 'Open Data Platform Waterschap',
|
||||
'beschrijving' => 'Ontwikkeling van een publiekstoegankelijk data-portaal voor het ontsluiten van historische en actuele meetdata van het waterschap.',
|
||||
'eigenaar_id' => $testUser->id,
|
||||
'status' => ProjectStatus::Geparkeerd,
|
||||
'prioriteit' => Prioriteit::Laag,
|
||||
'startdatum' => '2024-06-01',
|
||||
'streef_einddatum' => '2025-12-31',
|
||||
], [FaseType::Signaal, FaseType::Verkenning]);
|
||||
|
||||
// --- Duurzaamheid projects ---
|
||||
$p10 = $this->createProject([
|
||||
'speerpunt_id' => $spEnergieneutraal->id,
|
||||
'naam' => 'Biogasopwaardering RWZI Bath',
|
||||
'beschrijving' => 'Opwaardering van slibvergistingsgas naar groengas-kwaliteit voor invoeding op het gasnet en verkoop aan een energiemaatschappij.',
|
||||
'eigenaar_id' => $engineer->id,
|
||||
'status' => ProjectStatus::Evaluatie,
|
||||
'prioriteit' => Prioriteit::Midden,
|
||||
'startdatum' => '2024-01-01',
|
||||
'streef_einddatum' => '2025-06-30',
|
||||
], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment, FaseType::Pilot, FaseType::Besluitvorming, FaseType::OverdrachtBouwen, FaseType::OverdrachtBeheer, FaseType::Evaluatie]);
|
||||
|
||||
$p11 = $this->createProject([
|
||||
'speerpunt_id' => $spCirculaireWater->id,
|
||||
'naam' => 'Fosfaatterugwinning Struviet',
|
||||
'beschrijving' => 'Implementatie van struvietkristallisatie-technologie op RWZI Nieuw-Vossemeer voor de terugwinning van fosfaat als meststof.',
|
||||
'eigenaar_id' => $testUser->id,
|
||||
'status' => ProjectStatus::Pilot,
|
||||
'prioriteit' => Prioriteit::Midden,
|
||||
'startdatum' => '2025-02-01',
|
||||
'streef_einddatum' => '2026-02-28',
|
||||
], [FaseType::Signaal, FaseType::Verkenning, FaseType::Concept, FaseType::Experiment, FaseType::Pilot]);
|
||||
|
||||
$p12 = $this->createProject([
|
||||
'speerpunt_id' => $spEnergieneutraal->id,
|
||||
'naam' => 'Warmteterugwinning Afvalwater Centrum',
|
||||
'beschrijving' => 'Pilotinstallatie voor warmtewisselaars op het rioolstelsel in Breda-centrum om warmte terug te winnen voor stadsverwarming.',
|
||||
'eigenaar_id' => $engineer->id,
|
||||
// --- Structuur & Borging R&D-team ---
|
||||
$governance = $this->createProject([
|
||||
'speerpunt_id' => $spProjectstructuur->id,
|
||||
'naam' => 'Structuur & Borging R&D',
|
||||
'beschrijving' => 'R&D minder persoonsafhankelijk en beter voorspelbaar maken. Afspraken over projectstructuur, documentatie, besluitvorming, beleid. Inrichten ICT in Bouvigne voor R&D en Datanetwerkteam.',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'status' => ProjectStatus::Verkenning,
|
||||
'prioriteit' => Prioriteit::Midden,
|
||||
'startdatum' => '2025-10-01',
|
||||
'streef_einddatum' => '2027-03-31',
|
||||
'startdatum' => '2026-01-01',
|
||||
'streef_einddatum' => '2026-12-31',
|
||||
], [FaseType::Signaal, FaseType::Verkenning]);
|
||||
|
||||
$projects = [$p1, $p2, $p3, $p4, $p5, $p6, $p7, $p8, $p9, $p10, $p11, $p12];
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 6. Assign team members
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
$p1->teamleden()->attach($analyst->id, ['rol' => ProjectRol::Lid->value]);
|
||||
$p2->teamleden()->attach($testUser->id, ['rol' => ProjectRol::Reviewer->value]);
|
||||
$p5->teamleden()->attach($analyst->id, ['rol' => ProjectRol::Lid->value]);
|
||||
$p5->teamleden()->attach($engineer->id, ['rol' => ProjectRol::Reviewer->value]);
|
||||
$p8->teamleden()->attach($engineer->id, ['rol' => ProjectRol::Lid->value]);
|
||||
$bridge->teamleden()->attach($pim->id, ['rol' => ProjectRol::Lid->value]);
|
||||
$bridge->teamleden()->attach($sjoerd->id, ['rol' => ProjectRol::Lid->value]);
|
||||
$crisp->teamleden()->attach($rene->id, ['rol' => ProjectRol::Reviewer->value]);
|
||||
$wise->teamleden()->attach($pim->id, ['rol' => ProjectRol::Lid->value]);
|
||||
$gemaal->teamleden()->attach($rene->id, ['rol' => ProjectRol::Reviewer->value]);
|
||||
$afvlak->teamleden()->attach($rene->id, ['rol' => ProjectRol::Reviewer->value]);
|
||||
$governance->teamleden()->attach($pim->id, ['rol' => ProjectRol::Lid->value]);
|
||||
$governance->teamleden()->attach($sjoerd->id, ['rol' => ProjectRol::Lid->value]);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 7. Commitments
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
// BRIDGE
|
||||
Commitment::create([
|
||||
'project_id' => $p1->id,
|
||||
'beschrijving' => 'Installatie van 20 sensorknooppunten in het veld vóór einde Q3 2025',
|
||||
'eigenaar_id' => $testUser->id,
|
||||
'deadline' => '2025-09-30',
|
||||
'project_id' => $bridge->id,
|
||||
'beschrijving' => 'Architectuur validatiedocument opleveren conform stackKlundertPilot.pdf',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'deadline' => '2026-04-30',
|
||||
'status' => CommitmentStatus::InUitvoering,
|
||||
'bron' => 'Stuurgroep Waterkwaliteit — 12 maart 2025',
|
||||
'bron' => 'Planning 2026 – slide 3',
|
||||
]);
|
||||
|
||||
Commitment::create([
|
||||
'project_id' => $p1->id,
|
||||
'beschrijving' => 'Validatierapport sensornauwkeurigheid opleveren aan dataplatformteam',
|
||||
'eigenaar_id' => $analyst->id,
|
||||
'deadline' => '2025-11-30',
|
||||
'project_id' => $bridge->id,
|
||||
'beschrijving' => 'OT/IT beveiligingsassessment en scheidingsrapport',
|
||||
'eigenaar_id' => $pim->id,
|
||||
'deadline' => '2026-06-30',
|
||||
'status' => CommitmentStatus::Open,
|
||||
'bron' => 'Stuurgroep Waterkwaliteit — 12 maart 2025',
|
||||
'bron' => 'Planning 2026 – slide 3',
|
||||
]);
|
||||
|
||||
Commitment::create([
|
||||
'project_id' => $p5->id,
|
||||
'beschrijving' => 'Afstemming met Rijkswaterstaat over databeschikbaarheid afvoermetingen',
|
||||
'eigenaar_id' => $engineer->id,
|
||||
'deadline' => '2025-07-31',
|
||||
'status' => CommitmentStatus::Afgerond,
|
||||
'bron' => 'Projectstartup 15 januari 2025',
|
||||
]);
|
||||
|
||||
Commitment::create([
|
||||
'project_id' => $p5->id,
|
||||
'beschrijving' => 'Businesscase energiebesparing opstellen voor directie',
|
||||
'eigenaar_id' => $testUser->id,
|
||||
'deadline' => '2025-10-15',
|
||||
'project_id' => $bridge->id,
|
||||
'beschrijving' => 'Beslisdocument voor opschaling EDGE-laag naar andere locaties',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'deadline' => '2026-09-30',
|
||||
'status' => CommitmentStatus::Open,
|
||||
'bron' => 'Besluitvormingsrapport fase 5',
|
||||
'bron' => 'Planning 2026 – slide 3',
|
||||
]);
|
||||
|
||||
// CRISP
|
||||
Commitment::create([
|
||||
'project_id' => $crisp->id,
|
||||
'beschrijving' => 'TCO-onderbouwde aanbestedingsdocumenten technisch getoetst vóór marktgang',
|
||||
'eigenaar_id' => $pim->id,
|
||||
'deadline' => '2026-05-31',
|
||||
'status' => CommitmentStatus::Open,
|
||||
'bron' => 'Planning 2026 – slide 4',
|
||||
]);
|
||||
|
||||
// WISE
|
||||
Commitment::create([
|
||||
'project_id' => $wise->id,
|
||||
'beschrijving' => 'Node-RED flows opleveren aan Bouwen / uitvoerende partij',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'deadline' => '2026-03-31',
|
||||
'status' => CommitmentStatus::InUitvoering,
|
||||
'bron' => 'Planning 2026 – slide 5',
|
||||
]);
|
||||
|
||||
Commitment::create([
|
||||
'project_id' => $p8->id,
|
||||
'beschrijving' => 'Trainingsdata leveren: minimaal 5 jaar uurlijkse waterstandsmetingen',
|
||||
'eigenaar_id' => $analyst->id,
|
||||
'deadline' => '2025-08-01',
|
||||
'status' => CommitmentStatus::Afgerond,
|
||||
'bron' => 'Projectplan AI Waterstandsmodel v1.0',
|
||||
'project_id' => $wise->id,
|
||||
'beschrijving' => 'Overdracht- en documentatiepakket compleet (afbakening R&D vs beheer)',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'deadline' => '2026-06-30',
|
||||
'status' => CommitmentStatus::Open,
|
||||
'bron' => 'Planning 2026 – slide 5',
|
||||
]);
|
||||
|
||||
// Gemaal 3.0
|
||||
Commitment::create([
|
||||
'project_id' => $gemaal->id,
|
||||
'beschrijving' => 'Go/no-go besluit voor uitrol naar 2 kopieën op basis van testresultaten',
|
||||
'eigenaar_id' => $sjoerd->id,
|
||||
'deadline' => '2026-06-30',
|
||||
'status' => CommitmentStatus::Open,
|
||||
'bron' => 'Planning 2026 – slide 6',
|
||||
]);
|
||||
|
||||
Commitment::create([
|
||||
'project_id' => $p10->id,
|
||||
'beschrijving' => 'Eindrapportage energieopbrengst en milieuprestatie biogasinstallatie',
|
||||
'eigenaar_id' => $engineer->id,
|
||||
'deadline' => '2025-05-31',
|
||||
'status' => CommitmentStatus::Afgerond,
|
||||
'bron' => 'Evaluatieprogramma RWZI Bath',
|
||||
'project_id' => $gemaal->id,
|
||||
'beschrijving' => 'Overzicht software/hardware functionaliteiten en validatie digital twins',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'deadline' => '2026-07-31',
|
||||
'status' => CommitmentStatus::Open,
|
||||
'bron' => 'Planning 2026 – slide 6',
|
||||
]);
|
||||
|
||||
// Afvlakkingsregeling
|
||||
Commitment::create([
|
||||
'project_id' => $afvlak->id,
|
||||
'beschrijving' => '2 extra opstellingen gerealiseerd en gekoppeld tot keten',
|
||||
'eigenaar_id' => $sjoerd->id,
|
||||
'deadline' => '2026-10-31',
|
||||
'status' => CommitmentStatus::Open,
|
||||
'bron' => 'Planning 2026 – slide 7',
|
||||
]);
|
||||
|
||||
// Governance
|
||||
Commitment::create([
|
||||
'project_id' => $governance->id,
|
||||
'beschrijving' => 'Projectstructuur- en documentatieafspraken vastgelegd',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'deadline' => '2026-03-31',
|
||||
'status' => CommitmentStatus::InUitvoering,
|
||||
'bron' => 'Planning 2026 – slide 8',
|
||||
]);
|
||||
|
||||
Commitment::create([
|
||||
'project_id' => $p11->id,
|
||||
'beschrijving' => 'Technische specificaties struvietreactor aanleveren aan leverancier',
|
||||
'eigenaar_id' => $testUser->id,
|
||||
'deadline' => '2025-05-01',
|
||||
'status' => CommitmentStatus::Afgerond,
|
||||
'bron' => 'Pilotopzet Fosfaatterugwinning',
|
||||
'project_id' => $governance->id,
|
||||
'beschrijving' => 'Rolafbakening R&D ↔ Beheer ↔ Bouwen vastgelegd en gecommuniceerd',
|
||||
'eigenaar_id' => $rene->id,
|
||||
'deadline' => '2026-06-30',
|
||||
'status' => CommitmentStatus::Open,
|
||||
'bron' => 'Planning 2026 – slide 8',
|
||||
]);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 8. Documents
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
Document::create([
|
||||
'project_id' => $p1->id,
|
||||
'titel' => 'Technisch Ontwerp LoRaWAN Netwerk',
|
||||
'project_id' => $bridge->id,
|
||||
'titel' => 'Stack Architectuur Klundert Pilot',
|
||||
'type' => 'technisch_ontwerp',
|
||||
'inhoud' => 'Systeemarchitectuur, gatewaylocaties, frequentieplan en databeheerprotocol voor het LoRaWAN sensornetwerk in het Biesboschgebied.',
|
||||
'versie' => 2,
|
||||
'auteur_id' => $engineer->id,
|
||||
]);
|
||||
|
||||
Document::create([
|
||||
'project_id' => $p1->id,
|
||||
'titel' => 'Projectplan Fase Pilot',
|
||||
'type' => 'projectplan',
|
||||
'inhoud' => 'Doelstellingen, activiteiten, planning en risico\'s voor de pilotfase van het LoRaWAN sensornetwerk.',
|
||||
'inhoud' => 'Systeemarchitectuur EDGE-laag: Ubuntu LTS, Node-RED orkestratie, OPC UA communicatie met Siemens PLC T-serie, InfluxDB/Grafana datastromen.',
|
||||
'versie' => 1,
|
||||
'auteur_id' => $testUser->id,
|
||||
'auteur_id' => $rene->id,
|
||||
]);
|
||||
|
||||
Document::create([
|
||||
'project_id' => $p5->id,
|
||||
'titel' => 'Businesscase Predictieve Gemaalbesturing',
|
||||
'type' => 'businesscase',
|
||||
'inhoud' => 'Kosten-batenanalyse voor de implementatie van ML-gestuurde pompsturing, inclusief energiebesparingspotentieel en investeringskosten.',
|
||||
'versie' => 3,
|
||||
'auteur_id' => $testUser->id,
|
||||
]);
|
||||
|
||||
Document::create([
|
||||
'project_id' => $p8->id,
|
||||
'titel' => 'Modelarchitectuur LSTM Waterstandsvoorspelling',
|
||||
'type' => 'technisch_rapport',
|
||||
'inhoud' => 'Gedetailleerde beschrijving van de LSTM-netwerkarchitectuur, feature engineering, trainingsopzet en validatiemethodiek.',
|
||||
'versie' => 1,
|
||||
'auteur_id' => $analyst->id,
|
||||
]);
|
||||
|
||||
Document::create([
|
||||
'project_id' => $p10->id,
|
||||
'titel' => 'Evaluatierapport Biogasopwaardering RWZI Bath',
|
||||
'type' => 'evaluatierapport',
|
||||
'inhoud' => 'Eindresultaten van de biogasopwaarderingsinstallatie: energieproductie, CH4-gehalte, opbrengst en geleerde lessen.',
|
||||
'versie' => 1,
|
||||
'auteur_id' => $engineer->id,
|
||||
]);
|
||||
|
||||
Document::create([
|
||||
'project_id' => $p4->id,
|
||||
'titel' => 'BIM-protocol Kunstwerken v2.0',
|
||||
'project_id' => $wise->id,
|
||||
'titel' => 'Overdrachtsprotocol Monsternamekast',
|
||||
'type' => 'protocol',
|
||||
'inhoud' => 'Afspraken voor objectcodering, LOD-niveaus, attribuutvelden en uitwisselformaten (IFC) voor het BIM-model van kunstwerken.',
|
||||
'versie' => 2,
|
||||
'auteur_id' => $engineer->id,
|
||||
'inhoud' => 'Opleverdocument voor de overdracht van de monsternamekast naar Bouwen. Bevat: Node-RED flow specificaties, integratie voorspellend model, afbakening verantwoordelijkheden.',
|
||||
'versie' => 1,
|
||||
'auteur_id' => $rene->id,
|
||||
]);
|
||||
|
||||
Document::create([
|
||||
'project_id' => $crisp->id,
|
||||
'titel' => 'Technische Toetsing Aanbesteding Compressoren',
|
||||
'type' => 'technisch_rapport',
|
||||
'inhoud' => 'Q&A en technische beoordeling van aanbestedingsdocumenten voor compressorvervanging Nieuwveer. TCO-analyse, prestatie-eisen, energie-efficiëntie.',
|
||||
'versie' => 1,
|
||||
'auteur_id' => $pim->id,
|
||||
]);
|
||||
|
||||
Document::create([
|
||||
'project_id' => $gemaal->id,
|
||||
'titel' => 'Testrapport Prototype Gemaal 3.0',
|
||||
'type' => 'technisch_rapport',
|
||||
'inhoud' => 'Bevindingen en verbeterpunten uit validatie van het prototype: elektrische aansluitingen, I/O-functionaliteit, randapparatuur, digital twin koppelingen.',
|
||||
'versie' => 1,
|
||||
'auteur_id' => $sjoerd->id,
|
||||
]);
|
||||
|
||||
Document::create([
|
||||
'project_id' => $governance->id,
|
||||
'titel' => 'R&D Projectstructuur & Beleidskader',
|
||||
'type' => 'projectplan',
|
||||
'inhoud' => 'Afspraken over projectstructuur, documentatiestandaarden, besluitvormingsproces en beleid voor het R&D-lab.',
|
||||
'versie' => 1,
|
||||
'auteur_id' => $rene->id,
|
||||
]);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 9. Dependencies between projects
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
// AI Waterstandsmodel is afhankelijk van LoRaWAN Sensornetwerk (databron)
|
||||
// Afvlakkingsregeling hangt af van Gemaal 3.0 (prototype moet gevalideerd zijn)
|
||||
Afhankelijkheid::create([
|
||||
'project_id' => $p8->id,
|
||||
'afhankelijk_van_project_id' => $p1->id,
|
||||
'type' => 'data',
|
||||
'beschrijving' => 'Het AI-model heeft real-time sensordata nodig uit het LoRaWAN netwerk als input feature.',
|
||||
'status' => 'open',
|
||||
]);
|
||||
|
||||
// Digitale Tweeling is afhankelijk van AI Waterstandsmodel
|
||||
Afhankelijkheid::create([
|
||||
'project_id' => $p7->id,
|
||||
'afhankelijk_van_project_id' => $p8->id,
|
||||
'project_id' => $afvlak->id,
|
||||
'afhankelijk_van_project_id' => $gemaal->id,
|
||||
'type' => 'technisch',
|
||||
'beschrijving' => 'De digitale tweeling integreert de voorspellingsmodule van het AI waterstandsmodel.',
|
||||
'beschrijving' => 'De afvlakkingsregeling vereist gevalideerde prototypes van Gemaal 3.0 (× 2 kopieën) als basis voor de ketenopstelling.',
|
||||
'status' => 'open',
|
||||
]);
|
||||
|
||||
// Predictieve Gemaalbesturing is afhankelijk van AI Waterstandsmodel
|
||||
// WISE overdracht profiteert van BRIDGE architectuur (gedeelde EDGE-infrastructuur)
|
||||
Afhankelijkheid::create([
|
||||
'project_id' => $p5->id,
|
||||
'afhankelijk_van_project_id' => $p8->id,
|
||||
'type' => 'technisch',
|
||||
'beschrijving' => 'De predictieve besturing gebruikt de 48-uurs waterstandsvoorspellingen als stuurinput.',
|
||||
'status' => 'open',
|
||||
]);
|
||||
|
||||
// Remote Monitoring Waterkeringen is afhankelijk van LoRaWAN Sensornetwerk
|
||||
Afhankelijkheid::create([
|
||||
'project_id' => $p6->id,
|
||||
'afhankelijk_van_project_id' => $p1->id,
|
||||
'project_id' => $wise->id,
|
||||
'afhankelijk_van_project_id' => $bridge->id,
|
||||
'type' => 'infrastructuur',
|
||||
'beschrijving' => 'Het monitoring-project maakt gebruik van de LoRaWAN-infrastructuur voor datatransport.',
|
||||
'beschrijving' => 'De monsternamekast draait op dezelfde EDGE-architectuur die BRIDGE valideert.',
|
||||
'status' => 'open',
|
||||
]);
|
||||
|
||||
// Gemaal 3.0 validatie van digital twins hangt samen met BRIDGE architectuur
|
||||
Afhankelijkheid::create([
|
||||
'project_id' => $gemaal->id,
|
||||
'afhankelijk_van_project_id' => $bridge->id,
|
||||
'type' => 'technisch',
|
||||
'beschrijving' => 'Digital twin validatie in Gemaal 3.0 gebruikt dezelfde EDGE-laag als bewezen in BRIDGE.',
|
||||
'status' => 'open',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: create a project with its completed and active phases.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
* @param FaseType[] $faseTypes All phase types in chronological order; the last one is the active phase.
|
||||
*/
|
||||
private function createProject(array $attributes, array $faseTypes): Project
|
||||
{
|
||||
$project = Project::create($attributes);
|
||||
|
||||
// Attach the project owner as eigenaar in the pivot table
|
||||
$project->teamleden()->attach($attributes['eigenaar_id'], ['rol' => ProjectRol::Eigenaar->value]);
|
||||
|
||||
foreach ($faseTypes as $index => $faseType) {
|
||||
|
||||
BIN
database/seeders/data/raw/Planning 2026.pptx
Normal file
BIN
database/seeders/data/raw/Planning 2026.pptx
Normal file
Binary file not shown.
@@ -1,7 +1,18 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const emit = defineEmits(['create-project', 'create-theme'])
|
||||
const props = defineProps({
|
||||
depth: { type: Number, default: 1 },
|
||||
parentEntityType: { type: String, default: null },
|
||||
parentProjectId: { type: Number, default: null },
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'create-project',
|
||||
'create-theme',
|
||||
'create-commitment',
|
||||
'create-document',
|
||||
])
|
||||
|
||||
const menuOpen = ref(false)
|
||||
|
||||
@@ -9,14 +20,25 @@ const toggle = () => {
|
||||
menuOpen.value = !menuOpen.value
|
||||
}
|
||||
|
||||
const handleCreateProject = () => {
|
||||
menuOpen.value = false
|
||||
emit('create-project')
|
||||
/** Options change based on which dimension we're in */
|
||||
const menuItems = computed(() => {
|
||||
if (props.depth > 1 && props.parentEntityType === 'project') {
|
||||
// Inside a project dimension: create project-level items
|
||||
return [
|
||||
{ label: 'Nieuw commitment', event: 'create-commitment' },
|
||||
{ label: 'Nieuw document', event: 'create-document' },
|
||||
]
|
||||
}
|
||||
// Root dimension: create top-level items
|
||||
return [
|
||||
{ label: 'Nieuw project', event: 'create-project' },
|
||||
{ label: 'Nieuw thema', event: 'create-theme' },
|
||||
]
|
||||
})
|
||||
|
||||
const handleCreateTheme = () => {
|
||||
const handleItemClick = (item) => {
|
||||
menuOpen.value = false
|
||||
emit('create-theme')
|
||||
emit(item.event)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -25,13 +47,14 @@ const handleCreateTheme = () => {
|
||||
<!-- Expanded menu -->
|
||||
<Transition name="fab-menu">
|
||||
<div v-if="menuOpen" class="fab-menu">
|
||||
<button class="fab-menu-item" @click="handleCreateProject">
|
||||
<button
|
||||
v-for="item in menuItems"
|
||||
:key="item.event"
|
||||
class="fab-menu-item"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<span class="fab-menu-icon">+</span>
|
||||
<span class="fab-menu-label">Nieuw project</span>
|
||||
</button>
|
||||
<button class="fab-menu-item" @click="handleCreateTheme">
|
||||
<span class="fab-menu-icon">+</span>
|
||||
<span class="fab-menu-label">Nieuw thema</span>
|
||||
<span class="fab-menu-label">{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -51,7 +74,7 @@ const handleCreateTheme = () => {
|
||||
<style scoped>
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
bottom: 64px; /* above the CLI bar */
|
||||
bottom: 64px;
|
||||
right: 20px;
|
||||
z-index: 150;
|
||||
display: flex;
|
||||
|
||||
@@ -7,10 +7,10 @@ import * as d3 from 'd3'
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const props = defineProps({
|
||||
dimensions: { type: Object, default: () => ({}) }, // root dimension data
|
||||
currentPath: { type: Array, default: () => [] }, // breadcrumb path of dimension IDs
|
||||
dimensions: { type: Object, default: () => ({}) },
|
||||
currentPath: { type: Array, default: () => [] },
|
||||
|
||||
// Legacy flat-prop support (for backward compat with MetroMap.vue)
|
||||
// Legacy flat-prop support
|
||||
nodes: { type: Array, default: () => [] },
|
||||
lines: { type: Array, default: () => [] },
|
||||
connections: { type: Array, default: () => [] },
|
||||
@@ -33,8 +33,10 @@ const emit = defineEmits([
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ZOOM_IN_THRESHOLD = 2.5
|
||||
const ZOOM_OUT_THRESHOLD = 0.5
|
||||
const TRANSITION_RANGE = 1.5
|
||||
const ZOOM_OUT_THRESHOLD = 0.6
|
||||
const ZOOM_IN_COMMIT = 4.0 // commit at this scale (smoother range)
|
||||
const ZOOM_OUT_COMMIT = 0.25 // commit at this scale
|
||||
const NODE_PROXIMITY = 250 // how close to a node to trigger zoom-in
|
||||
|
||||
const LINE_COLORS = [
|
||||
'#00d2ff', '#e94560', '#00ff88', '#7b68ee',
|
||||
@@ -51,29 +53,32 @@ const containerRef = ref(null)
|
||||
const transform = ref(d3.zoomIdentity)
|
||||
const hoveredNode = ref(null)
|
||||
|
||||
// Dimension stack: each entry is a full metro-map dimension object
|
||||
// Dimension stack
|
||||
const dimensionStack = ref([])
|
||||
const currentDimensionIndex = ref(0)
|
||||
|
||||
// Transition state
|
||||
const transitionState = ref({
|
||||
active: false,
|
||||
direction: null, // 'in' | 'out'
|
||||
direction: null,
|
||||
progress: 0,
|
||||
targetNode: null,
|
||||
childDimension: null,
|
||||
})
|
||||
|
||||
// Tracks whether we're mid-commit (prevent re-entry)
|
||||
let isCommitting = false
|
||||
|
||||
// Context menu
|
||||
const contextMenu = ref({
|
||||
show: false,
|
||||
x: 0, y: 0,
|
||||
type: null, // 'canvas' | 'node'
|
||||
type: null,
|
||||
node: null,
|
||||
canvasX: 0, canvasY: 0,
|
||||
})
|
||||
|
||||
// D3 references (module-level, not reactive)
|
||||
// D3 references
|
||||
let svg, g, zoom
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -82,26 +87,30 @@ let svg, g, zoom
|
||||
|
||||
const getLineColor = (index) => LINE_COLORS[index % LINE_COLORS.length]
|
||||
|
||||
/** The dimension data that is currently "active" (top of stack) */
|
||||
const currentDimensionData = computed(() => {
|
||||
return dimensionStack.value[currentDimensionIndex.value] ?? null
|
||||
})
|
||||
|
||||
/** Flat list of nodes in the current dimension */
|
||||
const currentNodes = computed(() => {
|
||||
return currentDimensionData.value?.nodes ?? []
|
||||
})
|
||||
|
||||
/** Current depth (1-based) */
|
||||
const currentDepth = computed(() => currentDimensionIndex.value + 1)
|
||||
|
||||
/** Whether we're currently inside a child dimension (not root) */
|
||||
const isInChildDimension = computed(() => currentDimensionIndex.value > 0)
|
||||
|
||||
/** The parent node we zoomed into (if in child dimension) */
|
||||
const parentNode = computed(() => {
|
||||
if (!isInChildDimension.value) return null
|
||||
return currentDimensionData.value?.parentNodeId ?? null
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bootstrap dimension stack from props
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const buildRootDimension = () => {
|
||||
// If a structured `dimensions` prop is provided, use it as root.
|
||||
// Otherwise fall back to the legacy flat props (nodes/lines/connections).
|
||||
if (props.dimensions && (props.dimensions.nodes || props.dimensions.lines)) {
|
||||
return {
|
||||
id: 'root',
|
||||
@@ -154,7 +163,6 @@ const initCanvas = () => {
|
||||
feMerge.append('feMergeNode').attr('in', 'coloredBlur')
|
||||
feMerge.append('feMergeNode').attr('in', 'SourceGraphic')
|
||||
|
||||
// Subtle glow for child indicator
|
||||
const childGlowFilter = defs.append('filter')
|
||||
.attr('id', 'childGlow')
|
||||
.attr('x', '-100%').attr('y', '-100%')
|
||||
@@ -173,7 +181,7 @@ const initCanvas = () => {
|
||||
|
||||
// --- Zoom ---
|
||||
zoom = d3.zoom()
|
||||
.scaleExtent([0.1, 8])
|
||||
.scaleExtent([0.15, 6])
|
||||
.on('zoom', handleZoom)
|
||||
|
||||
svg.call(zoom)
|
||||
@@ -212,13 +220,14 @@ const initCanvas = () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const handleZoom = (event) => {
|
||||
if (isCommitting) return
|
||||
|
||||
const t = event.transform
|
||||
g.attr('transform', t)
|
||||
transform.value = t
|
||||
|
||||
emit('zoom-change', { scale: t.k, x: t.x, y: t.y })
|
||||
|
||||
// Find nearest node to the viewport centre (in canvas space)
|
||||
const w = containerRef.value?.clientWidth ?? 800
|
||||
const h = containerRef.value?.clientHeight ?? 600
|
||||
const cx = (w / 2 - t.x) / t.k
|
||||
@@ -229,9 +238,10 @@ const handleZoom = (event) => {
|
||||
if (t.k > ZOOM_IN_THRESHOLD &&
|
||||
nearest &&
|
||||
nearest.node.children &&
|
||||
nearest.distance < 200)
|
||||
nearest.distance < NODE_PROXIMITY)
|
||||
{
|
||||
const progress = Math.min(1, (t.k - ZOOM_IN_THRESHOLD) / TRANSITION_RANGE)
|
||||
const range = ZOOM_IN_COMMIT - ZOOM_IN_THRESHOLD
|
||||
const progress = Math.min(1, Math.max(0, (t.k - ZOOM_IN_THRESHOLD) / range))
|
||||
|
||||
transitionState.value = {
|
||||
active: true,
|
||||
@@ -251,10 +261,8 @@ const handleZoom = (event) => {
|
||||
|
||||
// --- Zoom-out transition ---
|
||||
if (t.k < ZOOM_OUT_THRESHOLD && dimensionStack.value.length > 1) {
|
||||
const progress = Math.min(
|
||||
1,
|
||||
(ZOOM_OUT_THRESHOLD - t.k) / (ZOOM_OUT_THRESHOLD - 0.1)
|
||||
)
|
||||
const range = ZOOM_OUT_THRESHOLD - ZOOM_OUT_COMMIT
|
||||
const progress = Math.min(1, Math.max(0, (ZOOM_OUT_THRESHOLD - t.k) / range))
|
||||
|
||||
transitionState.value = {
|
||||
active: true,
|
||||
@@ -284,50 +292,94 @@ const handleZoom = (event) => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const commitDimensionChange = (direction, node) => {
|
||||
if (isCommitting) return
|
||||
isCommitting = true
|
||||
|
||||
if (direction === 'in' && node) {
|
||||
const children = node.children
|
||||
const child = {
|
||||
id: node.id,
|
||||
parentNodeId: node.id,
|
||||
lines: node.children.lines ?? [],
|
||||
nodes: node.children.nodes ?? [],
|
||||
connections: node.children.connections ?? [],
|
||||
// Prefer metadata from backend, fall back to node's own entity info
|
||||
parentEntityType: children.parentEntityType ?? node.entityType ?? null,
|
||||
parentEntityId: children.parentEntityId ?? node.entityId ?? null,
|
||||
parentName: children.parentName ?? node.name ?? null,
|
||||
lines: children.lines ?? [],
|
||||
nodes: children.nodes ?? [],
|
||||
connections: children.connections ?? [],
|
||||
opacity: 1,
|
||||
}
|
||||
dimensionStack.value.push(child)
|
||||
currentDimensionIndex.value++
|
||||
|
||||
resetZoomToCenter()
|
||||
animateZoomReset()
|
||||
|
||||
transitionState.value.active = false
|
||||
renderMap()
|
||||
emit('dimension-change', {
|
||||
direction: 'in',
|
||||
node,
|
||||
depth: dimensionStack.value.length,
|
||||
dimension: child,
|
||||
})
|
||||
} else if (direction === 'out') {
|
||||
dimensionStack.value.pop()
|
||||
currentDimensionIndex.value = Math.max(0, currentDimensionIndex.value - 1)
|
||||
|
||||
resetZoomToCenter()
|
||||
animateZoomReset()
|
||||
|
||||
transitionState.value.active = false
|
||||
renderMap()
|
||||
emit('dimension-change', {
|
||||
direction: 'out',
|
||||
depth: dimensionStack.value.length,
|
||||
dimension: currentDimensionData.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resetZoomToCenter = () => {
|
||||
if (!svg || !zoom) return
|
||||
/**
|
||||
* Smoothly animate back to the default zoom (scale=1, centered).
|
||||
* This prevents the jarring snap that a hard reset causes.
|
||||
*/
|
||||
const animateZoomReset = () => {
|
||||
if (!svg || !zoom) {
|
||||
isCommitting = false
|
||||
renderMap()
|
||||
return
|
||||
}
|
||||
|
||||
const w = containerRef.value?.clientWidth ?? 800
|
||||
const h = containerRef.value?.clientHeight ?? 600
|
||||
svg.call(
|
||||
|
||||
// Temporarily disable the zoom handler to prevent re-entry during animation
|
||||
svg.on('.zoom', null)
|
||||
|
||||
svg.transition()
|
||||
.duration(400)
|
||||
.ease(d3.easeCubicOut)
|
||||
.call(
|
||||
zoom.transform,
|
||||
d3.zoomIdentity.translate(w / 2, h / 2).scale(1)
|
||||
)
|
||||
.on('end', () => {
|
||||
// Re-enable zoom handler
|
||||
svg.call(zoom)
|
||||
svg.on('contextmenu', (event) => {
|
||||
event.preventDefault()
|
||||
const [x, y] = d3.pointer(event, g.node())
|
||||
const clickedNode = findNodeAt(x, y)
|
||||
contextMenu.value = {
|
||||
show: true,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
type: clickedNode ? 'node' : 'canvas',
|
||||
node: clickedNode,
|
||||
canvasX: x,
|
||||
canvasY: y,
|
||||
}
|
||||
})
|
||||
isCommitting = false
|
||||
renderMap()
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -358,7 +410,6 @@ const findNodeAt = (x, y, dimData = null) => {
|
||||
// Rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Render a complete metro-map dimension into a D3 group element */
|
||||
const renderDimension = (dimData, opacity, parentGroup) => {
|
||||
if (!dimData) return
|
||||
|
||||
@@ -378,7 +429,20 @@ const renderDimension = (dimData, opacity, parentGroup) => {
|
||||
.filter(n => n.lineId === line.id)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
|
||||
if (lineNodes.length < 2) {
|
||||
// Still render a single node's line label
|
||||
if (lineNodes.length === 1) {
|
||||
group.append('text')
|
||||
.attr('x', lineNodes[0].x - 10)
|
||||
.attr('y', lineNodes[0].y - 35)
|
||||
.attr('fill', color)
|
||||
.attr('font-family', "'VT323', monospace")
|
||||
.attr('font-size', '16px')
|
||||
.attr('opacity', 0.85)
|
||||
.text(line.name)
|
||||
}
|
||||
if (lineNodes.length < 2) return
|
||||
}
|
||||
|
||||
const lineGen = d3.line()
|
||||
.x(d => d.x)
|
||||
@@ -489,7 +553,7 @@ const renderDimension = (dimData, opacity, parentGroup) => {
|
||||
})
|
||||
.attr('stroke-width', 2)
|
||||
|
||||
// Child-dimension indicator (pulsing ring for nodes that have children)
|
||||
// Child-dimension indicator
|
||||
nodeGroups.filter(d => d.children && (d.children.nodes?.length ?? 0) > 0)
|
||||
.append('circle')
|
||||
.attr('r', 16)
|
||||
@@ -533,7 +597,7 @@ const renderDimension = (dimData, opacity, parentGroup) => {
|
||||
.text(d => d.badge)
|
||||
}
|
||||
|
||||
/** Normal (no transition) render */
|
||||
/** Normal render */
|
||||
const renderMap = () => {
|
||||
if (!g) return
|
||||
g.selectAll('*').remove()
|
||||
@@ -550,9 +614,7 @@ const renderWithTransition = () => {
|
||||
const eased = d3.easeCubicInOut(progress)
|
||||
|
||||
if (direction === 'in') {
|
||||
// Parent fades out
|
||||
renderDimension(currentDimensionData.value, 1 - eased, g)
|
||||
// Child fades in
|
||||
if (childDimension) {
|
||||
renderDimension(
|
||||
{
|
||||
@@ -565,9 +627,7 @@ const renderWithTransition = () => {
|
||||
)
|
||||
}
|
||||
} else if (direction === 'out' && dimensionStack.value.length > 1) {
|
||||
// Current child fades out
|
||||
renderDimension(currentDimensionData.value, 1 - eased, g)
|
||||
// Parent fades in
|
||||
const parentDim = dimensionStack.value[currentDimensionIndex.value - 1]
|
||||
if (parentDim) {
|
||||
renderDimension(parentDim, eased, g)
|
||||
@@ -588,7 +648,10 @@ const handleContextCreate = () => {
|
||||
x: contextMenu.value.canvasX,
|
||||
y: contextMenu.value.canvasY,
|
||||
parentNodeId: currentDimensionData.value?.parentNodeId ?? null,
|
||||
parentEntityType: currentDimensionData.value?.parentEntityType ?? null,
|
||||
parentEntityId: currentDimensionData.value?.parentEntityId ?? null,
|
||||
dimensionId: currentDimensionData.value?.id ?? 'root',
|
||||
depth: currentDepth.value,
|
||||
})
|
||||
closeContextMenu()
|
||||
}
|
||||
@@ -604,7 +667,10 @@ const handleContextAddChild = () => {
|
||||
x: contextMenu.value.canvasX,
|
||||
y: contextMenu.value.canvasY,
|
||||
parentNodeId: contextMenu.value.node.id,
|
||||
parentEntityType: contextMenu.value.node.entityType ?? null,
|
||||
parentEntityId: contextMenu.value.node.entityId ?? null,
|
||||
dimensionId: contextMenu.value.node.id,
|
||||
depth: currentDepth.value + 1,
|
||||
addToNode: contextMenu.value.node,
|
||||
})
|
||||
}
|
||||
@@ -616,12 +682,15 @@ const handleContextDelete = () => {
|
||||
closeContextMenu()
|
||||
}
|
||||
|
||||
/** FAB / [+] button creates a node in the current dimension */
|
||||
/** FAB creates a node in the current dimension */
|
||||
const handleFabCreate = () => {
|
||||
emit('create-node', {
|
||||
x: 0, y: 0,
|
||||
parentNodeId: currentDimensionData.value?.parentNodeId ?? null,
|
||||
parentEntityType: currentDimensionData.value?.parentEntityType ?? null,
|
||||
parentEntityId: currentDimensionData.value?.parentEntityId ?? null,
|
||||
dimensionId: currentDimensionData.value?.id ?? 'root',
|
||||
depth: currentDepth.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -640,7 +709,6 @@ const handleResize = () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const onDocumentClick = (e) => {
|
||||
// Close context menu on any click outside it
|
||||
if (contextMenu.value.show) {
|
||||
closeContextMenu()
|
||||
}
|
||||
@@ -653,7 +721,6 @@ const onDocumentClick = (e) => {
|
||||
watch(
|
||||
() => [props.nodes, props.lines, props.connections, props.dimensions],
|
||||
() => {
|
||||
// Rebuild the root dimension from updated props
|
||||
if (dimensionStack.value.length > 0) {
|
||||
dimensionStack.value[0] = buildRootDimension()
|
||||
} else {
|
||||
@@ -693,7 +760,13 @@ const zoomTo = (x, y, scale) => {
|
||||
)
|
||||
}
|
||||
|
||||
defineExpose({ zoomTo, handleFabCreate })
|
||||
defineExpose({
|
||||
zoomTo,
|
||||
handleFabCreate,
|
||||
currentDepth,
|
||||
currentDimensionData,
|
||||
isInChildDimension,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -706,7 +779,7 @@ defineExpose({ zoomTo, handleFabCreate })
|
||||
<div class="depth-indicator">
|
||||
DEPTH: {{ currentDepth }}
|
||||
<span v-if="currentDepth > 1" class="depth-back" @click.stop="commitDimensionChange('out', null)">
|
||||
[↑ BACK]
|
||||
[↑ BACK]
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -746,13 +819,23 @@ defineExpose({ zoomTo, handleFabCreate })
|
||||
@click.stop
|
||||
>
|
||||
<template v-if="contextMenu.type === 'canvas'">
|
||||
<div class="context-menu-header">CANVAS</div>
|
||||
<button class="context-item" @click="handleContextCreate">+ New node here</button>
|
||||
<div class="context-menu-header">
|
||||
{{ currentDepth > 1 ? 'PROJECT' : 'CANVAS' }}
|
||||
</div>
|
||||
<button class="context-item" @click="handleContextCreate">
|
||||
+ {{ currentDepth > 1 ? 'New item here' : 'New node here' }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="contextMenu.type === 'node'">
|
||||
<div class="context-menu-header">{{ contextMenu.node?.name ?? 'NODE' }}</div>
|
||||
<button class="context-item" @click="handleContextEdit">Edit</button>
|
||||
<button class="context-item" @click="handleContextAddChild">+ Add child node</button>
|
||||
<button
|
||||
v-if="contextMenu.node?.children"
|
||||
class="context-item"
|
||||
@click="handleContextAddChild"
|
||||
>
|
||||
+ Add child node
|
||||
</button>
|
||||
<button class="context-item danger" @click="handleContextDelete">Delete</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -17,34 +17,62 @@ const props = defineProps({
|
||||
speerpunten: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
// Canvas ref
|
||||
const canvasRef = ref(null)
|
||||
|
||||
// Navigation state
|
||||
const selectedNode = ref(null)
|
||||
const showPreview = ref(false)
|
||||
|
||||
// Reactive breadcrumb based on mapData level and project
|
||||
// Dimension tracking (synced from canvas zoom transitions)
|
||||
const canvasDepth = ref(1)
|
||||
const canvasDimension = ref(null)
|
||||
|
||||
// Reactive breadcrumb based on both page-level and canvas-level navigation
|
||||
const breadcrumbPath = computed(() => {
|
||||
const level = props.mapData.level ?? 1
|
||||
const pageLevel = props.mapData.level ?? 1
|
||||
const project = props.mapData.project ?? null
|
||||
const path = [{ label: 'Strategie', level: 1, data: null }]
|
||||
if (level === 2 && project) {
|
||||
path.push({ label: project.name ?? project, level: 2, data: project })
|
||||
|
||||
if (pageLevel === 2 && project) {
|
||||
// Page-level project view
|
||||
path.push({ label: project.naam ?? project.name ?? 'Project', level: 2, data: project })
|
||||
} else if (canvasDepth.value > 1 && canvasDimension.value) {
|
||||
// Canvas zoom-in dimension
|
||||
path.push({
|
||||
label: canvasDimension.value.parentName ?? 'Detail',
|
||||
level: 2,
|
||||
data: canvasDimension.value,
|
||||
})
|
||||
}
|
||||
return path
|
||||
})
|
||||
|
||||
// Dimension-aware project ID: from page props OR from canvas zoom
|
||||
const currentProjectId = computed(() => {
|
||||
// Page-level project
|
||||
if (props.mapData.project?.id) return props.mapData.project.id
|
||||
// Canvas zoom-level project
|
||||
if (canvasDimension.value?.parentEntityType === 'project') {
|
||||
return canvasDimension.value.parentEntityId
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// Entity type in current dimension (for FAB awareness)
|
||||
const currentParentEntityType = computed(() => {
|
||||
if (props.mapData.level === 2) return 'project'
|
||||
return canvasDimension.value?.parentEntityType ?? null
|
||||
})
|
||||
|
||||
// Handlers
|
||||
const handleNodeClick = (node) => {
|
||||
selectedNode.value = node
|
||||
showPreview.value = true
|
||||
}
|
||||
|
||||
const handleNodeHover = (node) => {
|
||||
// Could highlight related nodes/connections
|
||||
}
|
||||
|
||||
const handleNodeLeave = () => {
|
||||
// Reset highlights
|
||||
}
|
||||
const handleNodeHover = (node) => {}
|
||||
const handleNodeLeave = () => {}
|
||||
|
||||
const handleZoomIn = (node) => {
|
||||
showPreview.value = false
|
||||
@@ -61,9 +89,17 @@ const handleBreadcrumbNavigate = (item, index) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDimensionChange = (event) => {
|
||||
canvasDepth.value = event.depth
|
||||
canvasDimension.value = event.dimension ?? null
|
||||
|
||||
// Close preview when dimension changes
|
||||
showPreview.value = false
|
||||
selectedNode.value = null
|
||||
}
|
||||
|
||||
const handleCliCommand = (command) => {
|
||||
console.log('CLI command:', command)
|
||||
// Will be connected to AI service
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
@@ -71,7 +107,6 @@ const logout = () => {
|
||||
}
|
||||
|
||||
const user = computed(() => page.props.auth?.user)
|
||||
|
||||
const hasNodes = computed(() => props.mapData.nodes && props.mapData.nodes.length > 0)
|
||||
|
||||
// Modal state
|
||||
@@ -88,8 +123,10 @@ const handleCreateCommitment = () => {
|
||||
showCommitmentForm.value = true
|
||||
}
|
||||
|
||||
// Get current project ID when at level 2
|
||||
const currentProjectId = computed(() => props.mapData.project?.id ?? null)
|
||||
const handleCreateDocument = () => {
|
||||
// Future: document upload modal
|
||||
console.log('Create document in project:', currentProjectId.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -108,6 +145,7 @@ const currentProjectId = computed(() => props.mapData.project?.id ?? null)
|
||||
|
||||
<template v-if="hasNodes">
|
||||
<MetroCanvas
|
||||
ref="canvasRef"
|
||||
:nodes="props.mapData.nodes"
|
||||
:lines="props.mapData.lines"
|
||||
:connections="props.mapData.connections"
|
||||
@@ -115,6 +153,7 @@ const currentProjectId = computed(() => props.mapData.project?.id ?? null)
|
||||
@node-click="handleNodeClick"
|
||||
@node-hover="handleNodeHover"
|
||||
@node-leave="handleNodeLeave"
|
||||
@dimension-change="handleDimensionChange"
|
||||
/>
|
||||
|
||||
<NodePreview
|
||||
@@ -130,8 +169,13 @@ const currentProjectId = computed(() => props.mapData.project?.id ?? null)
|
||||
</div>
|
||||
|
||||
<FloatingActions
|
||||
:depth="canvasDepth"
|
||||
:parent-entity-type="currentParentEntityType"
|
||||
:parent-project-id="currentProjectId"
|
||||
@create-project="handleCreateProject"
|
||||
@create-theme="handleCreateProject"
|
||||
@create-commitment="handleCreateCommitment"
|
||||
@create-document="handleCreateDocument"
|
||||
/>
|
||||
|
||||
<ProjectForm
|
||||
@@ -212,7 +256,7 @@ const currentProjectId = computed(() => props.mapData.project?.id ?? null)
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding-top: 48px; /* account for top-bar */
|
||||
padding-top: 48px;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
|
||||
Reference in New Issue
Block a user