Scaffold full project: Vue 3/Inertia frontend, Docker infra, domain model

- Frontend: Vue 3 + Inertia.js + Pinia + Tailwind CSS with layout and dashboard page
- Infrastructure: Docker Compose with nginx, PHP-FPM, PostgreSQL+pgvector, Redis, Python AI service
- Database: 22 migrations covering all domain entities (projects, phases, commitments, decisions, documents, handover, audit)
- Models: 23 Eloquent models with relationships, casts, and 14 string-backed enums
- AI service: FastAPI scaffold with health, chat, summarize, and search endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-04-01 12:49:20 +02:00
parent 46a1279cd6
commit b71b274361
81 changed files with 4700 additions and 39 deletions

View File

@@ -20,12 +20,12 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
DB_CONNECTION=pgsql
DB_HOST=postgresql
DB_PORT=5432
DB_DATABASE=innovatieplatform
DB_USERNAME=innovatie
DB_PASSWORD=secret
SESSION_DRIVER=database
SESSION_LIFETIME=120
@@ -43,7 +43,7 @@ CACHE_STORE=database
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
@@ -63,3 +63,5 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
AI_SERVICE_URL=http://ai-service:8000

21
ai-service/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3.12-slim
# Set working directory
WORKDIR /app
# Install system dependencies needed for psycopg2
RUN apt-get update && apt-get install -y \
libpq-dev \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

View File

117
ai-service/app/main.py Normal file
View File

@@ -0,0 +1,117 @@
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional
import os
app = FastAPI(
title="Innovatieplatform AI Service",
description="AI service providing chat, summarization and semantic search for the Innovatieplatform.",
version="0.1.0",
)
# CORS — allow requests from the Laravel app and local development
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://laravel-app",
"http://nginx",
"http://localhost",
"http://localhost:80",
os.getenv("LARAVEL_APP_URL", "http://localhost"),
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --- Request/Response models ---
class ChatRequest(BaseModel):
message: str
project_id: Optional[int] = None
conversation_history: Optional[list] = []
class ChatResponse(BaseModel):
reply: str
project_id: Optional[int] = None
class SummarizeRequest(BaseModel):
content: str
project_id: Optional[int] = None
summary_type: Optional[str] = "general"
class SummarizeResponse(BaseModel):
summary: str
project_id: Optional[int] = None
class SearchRequest(BaseModel):
query: str
project_id: Optional[int] = None
limit: Optional[int] = 10
class SearchResult(BaseModel):
id: int
content: str
score: float
metadata: Optional[dict] = {}
class SearchResponse(BaseModel):
results: list[SearchResult]
query: str
# --- Endpoints ---
@app.get("/health")
async def health_check():
"""Health check endpoint used by Docker and monitoring."""
return {"status": "ok", "service": "ai-service"}
@app.post("/api/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
"""
Handle a chat message, optionally scoped to a project.
Placeholder — wire up LangGraph + Anthropic in the full implementation.
"""
# TODO: integrate LangGraph agent with Anthropic Claude
reply = (
f"[AI placeholder] Received: '{request.message}'. "
"Full AI integration pending."
)
return ChatResponse(reply=reply, project_id=request.project_id)
@app.post("/api/summarize", response_model=SummarizeResponse)
async def summarize(request: SummarizeRequest):
"""
Summarize content for a given project.
Placeholder — wire up Anthropic in the full implementation.
"""
# TODO: integrate Anthropic Claude summarization
summary = (
f"[AI placeholder] Summary of {len(request.content)} characters "
f"(type: {request.summary_type}). Full AI integration pending."
)
return SummarizeResponse(summary=summary, project_id=request.project_id)
@app.post("/api/search", response_model=SearchResponse)
async def search(request: SearchRequest):
"""
Semantic search using pgvector embeddings.
Placeholder — wire up pgvector + embeddings in the full implementation.
"""
# TODO: integrate pgvector similarity search with embeddings
return SearchResponse(
results=[],
query=request.query,
)

View File

@@ -0,0 +1,11 @@
fastapi>=0.110.0
uvicorn>=0.27.0
langgraph>=0.0.40
langchain>=0.1.0
anthropic>=0.30.0
pgvector>=0.2.0
psycopg2-binary>=2.9.0
numpy>=1.26.0
pydantic>=2.0.0
python-dotenv>=1.0.0
httpx>=0.27.0

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum AcceptatieStatus: string
{
case Geaccepteerd = 'geaccepteerd';
case Afgewezen = 'afgewezen';
case Voorwaardelijk = 'voorwaardelijk';
}

10
app/Enums/ActieStatus.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum ActieStatus: string
{
case Open = 'open';
case InUitvoering = 'in_uitvoering';
case Afgerond = 'afgerond';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum BesluitStatus: string
{
case Concept = 'concept';
case Voorgelegd = 'voorgelegd';
case Goedgekeurd = 'goedgekeurd';
case Afgewezen = 'afgewezen';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum BudgetStatus: string
{
case Aangevraagd = 'aangevraagd';
case Toegekend = 'toegekend';
case Uitgeput = 'uitgeput';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum CommitmentStatus: string
{
case Open = 'open';
case InUitvoering = 'in_uitvoering';
case Afgerond = 'afgerond';
case Verlopen = 'verlopen';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum CriteriumStatus: string
{
case Open = 'open';
case Voldaan = 'voldaan';
case NietVoldaan = 'niet_voldaan';
}

10
app/Enums/FaseStatus.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum FaseStatus: string
{
case Open = 'open';
case Actief = 'actief';
case Afgerond = 'afgerond';
}

16
app/Enums/FaseType.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
namespace App\Enums;
enum FaseType: string
{
case Signaal = 'signaal';
case Verkenning = 'verkenning';
case Concept = 'concept';
case Experiment = 'experiment';
case Pilot = 'pilot';
case Besluitvorming = 'besluitvorming';
case OverdrachtBouwen = 'overdracht_bouwen';
case OverdrachtBeheer = 'overdracht_beheer';
case Evaluatie = 'evaluatie';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum OverdrachtsStatus: string
{
case Concept = 'concept';
case InUitvoering = 'in_uitvoering';
case Afgerond = 'afgerond';
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum OverdrachtsType: string
{
case NaarBouwen = 'naar_bouwen';
case NaarBeheer = 'naar_beheer';
}

10
app/Enums/Prioriteit.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum Prioriteit: string
{
case Laag = 'laag';
case Midden = 'midden';
case Hoog = 'hoog';
}

11
app/Enums/ProjectRol.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum ProjectRol: string
{
case Eigenaar = 'eigenaar';
case Lid = 'lid';
case Reviewer = 'reviewer';
case Stakeholder = 'stakeholder';
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Enums;
enum ProjectStatus: string
{
case Signaal = 'signaal';
case Verkenning = 'verkenning';
case Concept = 'concept';
case Experiment = 'experiment';
case Pilot = 'pilot';
case Besluitvorming = 'besluitvorming';
case OverdrachtBouwen = 'overdracht_bouwen';
case OverdrachtBeheer = 'overdracht_beheer';
case Evaluatie = 'evaluatie';
case Geparkeerd = 'geparkeerd';
case Gestopt = 'gestopt';
case Afgerond = 'afgerond';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum SpeerpuntStatus: string
{
case Concept = 'concept';
case Actief = 'actief';
case Afgerond = 'afgerond';
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that's loaded on the first page visit.
*
* @see https://inertiajs.com/server-side-setup#root-template
*
* @var string
*/
protected $rootView = 'app';
/**
* Determines the current asset version.
*
* @see https://inertiajs.com/asset-versioning
*/
public function version(Request $request): ?string
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @see https://inertiajs.com/shared-data
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
return [
...parent::share($request),
//
];
}
}

38
app/Models/Acceptatie.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use App\Enums\AcceptatieStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Acceptatie extends Model
{
protected $table = 'acceptaties';
protected $fillable = [
'overdrachtsplan_id',
'datum',
'door_id',
'opmerkingen',
'status',
];
protected function casts(): array
{
return [
'status' => AcceptatieStatus::class,
'datum' => 'date',
];
}
public function overdrachtsplan(): BelongsTo
{
return $this->belongsTo(Overdrachtsplan::class);
}
public function door(): BelongsTo
{
return $this->belongsTo(User::class, 'door_id');
}
}

39
app/Models/Actie.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use App\Enums\ActieStatus;
use App\Enums\Prioriteit;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Actie extends Model
{
protected $fillable = [
'commitment_id',
'beschrijving',
'eigenaar_id',
'deadline',
'status',
'prioriteit',
];
protected function casts(): array
{
return [
'status' => ActieStatus::class,
'prioriteit' => Prioriteit::class,
'deadline' => 'date',
];
}
public function commitment(): BelongsTo
{
return $this->belongsTo(Commitment::class);
}
public function eigenaar(): BelongsTo
{
return $this->belongsTo(User::class, 'eigenaar_id');
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Afhankelijkheid extends Model
{
protected $fillable = [
'project_id',
'afhankelijk_van_project_id',
'type',
'beschrijving',
'status',
];
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function afhankelijkVan(): BelongsTo
{
return $this->belongsTo(Project::class, 'afhankelijk_van_project_id');
}
}

33
app/Models/AuditLog.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AuditLog extends Model
{
// Append-only: no updated_at
const UPDATED_AT = null;
protected $fillable = [
'user_id',
'action',
'entity_type',
'entity_id',
'payload',
];
protected function casts(): array
{
return [
'payload' => 'array',
'created_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

41
app/Models/Besluit.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
namespace App\Models;
use App\Enums\BesluitStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Besluit extends Model
{
protected $table = 'besluiten';
protected $fillable = [
'project_id',
'titel',
'beschrijving',
'datum',
'type',
'status',
'onderbouwing',
];
protected function casts(): array
{
return [
'status' => BesluitStatus::class,
'datum' => 'date',
];
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function commitments(): HasMany
{
return $this->hasMany(Commitment::class);
}
}

32
app/Models/Besteding.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Besteding extends Model
{
protected $table = 'bestedingen';
protected $fillable = [
'budget_id',
'bedrag',
'beschrijving',
'datum',
'categorie',
];
protected function casts(): array
{
return [
'bedrag' => 'decimal:2',
'datum' => 'date',
];
}
public function budget(): BelongsTo
{
return $this->belongsTo(Budget::class);
}
}

37
app/Models/Budget.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use App\Enums\BudgetStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Budget extends Model
{
protected $fillable = [
'project_id',
'bedrag',
'type',
'periode',
'status',
];
protected function casts(): array
{
return [
'status' => BudgetStatus::class,
'bedrag' => 'decimal:2',
];
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function bestedingen(): HasMany
{
return $this->hasMany(Besteding::class);
}
}

49
app/Models/Commitment.php Normal file
View File

@@ -0,0 +1,49 @@
<?php
namespace App\Models;
use App\Enums\CommitmentStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Commitment extends Model
{
protected $fillable = [
'project_id',
'besluit_id',
'beschrijving',
'eigenaar_id',
'deadline',
'status',
'bron',
];
protected function casts(): array
{
return [
'status' => CommitmentStatus::class,
'deadline' => 'date',
];
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function besluit(): BelongsTo
{
return $this->belongsTo(Besluit::class);
}
public function eigenaar(): BelongsTo
{
return $this->belongsTo(User::class, 'eigenaar_id');
}
public function acties(): HasMany
{
return $this->hasMany(Actie::class);
}
}

31
app/Models/Criterium.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use App\Enums\CriteriumStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Criterium extends Model
{
protected $table = 'criteria';
protected $fillable = [
'overdrachtsplan_id',
'beschrijving',
'status',
'verificatie',
];
protected function casts(): array
{
return [
'status' => CriteriumStatus::class,
];
}
public function overdrachtsplan(): BelongsTo
{
return $this->belongsTo(Overdrachtsplan::class);
}
}

51
app/Models/Document.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Document extends Model
{
use SoftDeletes;
protected $fillable = [
'project_id',
'fase_id',
'titel',
'type',
'inhoud',
'bestandspad',
'versie',
'auteur_id',
];
protected function casts(): array
{
return [
'versie' => 'integer',
];
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function fase(): BelongsTo
{
return $this->belongsTo(Fase::class);
}
public function auteur(): BelongsTo
{
return $this->belongsTo(User::class, 'auteur_id');
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
}

46
app/Models/Fase.php Normal file
View File

@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use App\Enums\FaseStatus;
use App\Enums\FaseType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Fase extends Model
{
protected $fillable = [
'project_id',
'type',
'status',
'startdatum',
'einddatum',
'opmerkingen',
];
protected function casts(): array
{
return [
'type' => FaseType::class,
'status' => FaseStatus::class,
'startdatum' => 'date',
'einddatum' => 'date',
];
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function documents(): HasMany
{
return $this->hasMany(Document::class);
}
public function lessonsLearned(): HasMany
{
return $this->hasMany(LessonLearned::class);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class KennisArtikel extends Model
{
use SoftDeletes;
protected $table = 'kennis_artikelen';
protected $fillable = [
'titel',
'inhoud',
'auteur_id',
];
public function auteur(): BelongsTo
{
return $this->belongsTo(User::class, 'auteur_id');
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class, 'kennis_artikel_tag');
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class LessonLearned extends Model
{
protected $table = 'lessons_learned';
protected $fillable = [
'project_id',
'fase_id',
'titel',
'inhoud',
];
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function fase(): BelongsTo
{
return $this->belongsTo(Fase::class);
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class, 'lesson_learned_tag');
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Models;
use App\Enums\OverdrachtsStatus;
use App\Enums\OverdrachtsType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Overdrachtsplan extends Model
{
protected $table = 'overdrachtsplannen';
protected $fillable = [
'project_id',
'type',
'status',
'eigenaar_rnd_id',
'eigenaar_ontvanger_id',
];
protected function casts(): array
{
return [
'type' => OverdrachtsType::class,
'status' => OverdrachtsStatus::class,
];
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function eigenaarRnd(): BelongsTo
{
return $this->belongsTo(User::class, 'eigenaar_rnd_id');
}
public function eigenaarOntvanger(): BelongsTo
{
return $this->belongsTo(User::class, 'eigenaar_ontvanger_id');
}
public function criteria(): HasMany
{
return $this->hasMany(Criterium::class);
}
public function acceptaties(): HasMany
{
return $this->hasMany(Acceptatie::class);
}
}

106
app/Models/Project.php Normal file
View File

@@ -0,0 +1,106 @@
<?php
namespace App\Models;
use App\Enums\Prioriteit;
use App\Enums\ProjectRol;
use App\Enums\ProjectStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Project extends Model
{
use SoftDeletes;
protected $fillable = [
'speerpunt_id',
'naam',
'beschrijving',
'eigenaar_id',
'status',
'prioriteit',
'startdatum',
'streef_einddatum',
];
protected function casts(): array
{
return [
'status' => ProjectStatus::class,
'prioriteit' => Prioriteit::class,
'startdatum' => 'date',
'streef_einddatum' => 'date',
];
}
public function speerpunt(): BelongsTo
{
return $this->belongsTo(Speerpunt::class);
}
public function eigenaar(): BelongsTo
{
return $this->belongsTo(User::class, 'eigenaar_id');
}
public function teamleden(): BelongsToMany
{
return $this->belongsToMany(User::class)
->withPivot('rol')
->withTimestamps()
->using(ProjectUser::class);
}
public function fases(): HasMany
{
return $this->hasMany(Fase::class);
}
public function risicos(): HasMany
{
return $this->hasMany(Risico::class);
}
public function afhankelijkheden(): HasMany
{
return $this->hasMany(Afhankelijkheid::class);
}
public function afhankelijkVanMij(): HasMany
{
return $this->hasMany(Afhankelijkheid::class, 'afhankelijk_van_project_id');
}
public function besluiten(): HasMany
{
return $this->hasMany(Besluit::class);
}
public function commitments(): HasMany
{
return $this->hasMany(Commitment::class);
}
public function budgets(): HasMany
{
return $this->hasMany(Budget::class);
}
public function documents(): HasMany
{
return $this->hasMany(Document::class);
}
public function lessonsLearned(): HasMany
{
return $this->hasMany(LessonLearned::class);
}
public function overdrachtsplannen(): HasMany
{
return $this->hasMany(Overdrachtsplan::class);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use App\Enums\ProjectRol;
use Illuminate\Database\Eloquent\Relations\Pivot;
class ProjectUser extends Pivot
{
protected $table = 'project_user';
public $incrementing = false;
protected $fillable = [
'project_id',
'user_id',
'rol',
];
protected function casts(): array
{
return [
'rol' => ProjectRol::class,
];
}
}

37
app/Models/Risico.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use App\Enums\Prioriteit;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Risico extends Model
{
protected $fillable = [
'project_id',
'beschrijving',
'impact',
'kans',
'mitigatie',
'eigenaar_id',
];
protected function casts(): array
{
return [
'impact' => Prioriteit::class,
'kans' => Prioriteit::class,
];
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function eigenaar(): BelongsTo
{
return $this->belongsTo(User::class, 'eigenaar_id');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class RoadmapItem extends Model
{
protected $fillable = [
'thema_id',
'titel',
'start',
'eind',
'type',
'status',
];
protected function casts(): array
{
return [
'start' => 'date',
'eind' => 'date',
];
}
public function thema(): BelongsTo
{
return $this->belongsTo(Thema::class);
}
}

27
app/Models/Role.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
protected $fillable = [
'naam',
'beschrijving',
'permissies',
];
protected function casts(): array
{
return [
'permissies' => 'array',
];
}
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}

44
app/Models/Speerpunt.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use App\Enums\SpeerpuntStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Speerpunt extends Model
{
use SoftDeletes;
protected $fillable = [
'thema_id',
'naam',
'beschrijving',
'eigenaar_id',
'status',
];
protected function casts(): array
{
return [
'status' => SpeerpuntStatus::class,
];
}
public function thema(): BelongsTo
{
return $this->belongsTo(Thema::class);
}
public function eigenaar(): BelongsTo
{
return $this->belongsTo(User::class, 'eigenaar_id');
}
public function projects(): HasMany
{
return $this->hasMany(Project::class);
}
}

29
app/Models/Tag.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Tag extends Model
{
protected $fillable = [
'naam',
'categorie',
];
public function documents(): BelongsToMany
{
return $this->belongsToMany(Document::class);
}
public function kennisArtikelen(): BelongsToMany
{
return $this->belongsToMany(KennisArtikel::class, 'kennis_artikel_tag');
}
public function lessonsLearned(): BelongsToMany
{
return $this->belongsToMany(LessonLearned::class, 'lesson_learned_tag');
}
}

40
app/Models/Thema.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use App\Enums\Prioriteit;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Thema extends Model
{
use SoftDeletes;
protected $fillable = [
'naam',
'beschrijving',
'prioriteit',
'periode_start',
'periode_eind',
];
protected function casts(): array
{
return [
'prioriteit' => Prioriteit::class,
'periode_start' => 'date',
'periode_eind' => 'date',
];
}
public function speerpunten(): HasMany
{
return $this->hasMany(Speerpunt::class);
}
public function roadmapItems(): HasMany
{
return $this->hasMany(RoadmapItem::class);
}
}

View File

@@ -2,31 +2,87 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\ProjectRol;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected $fillable = [
'name',
'email',
'password',
'functie',
'afdeling',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'password' => 'hashed',
];
}
// Relationships
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
public function projects(): BelongsToMany
{
return $this->belongsToMany(Project::class)
->withPivot('rol')
->withTimestamps()
->using(ProjectUser::class);
}
public function eigenProjecten(): HasMany
{
return $this->hasMany(Project::class, 'eigenaar_id');
}
public function eigenSpeerpunten(): HasMany
{
return $this->hasMany(Speerpunt::class, 'eigenaar_id');
}
public function commitments(): HasMany
{
return $this->hasMany(Commitment::class, 'eigenaar_id');
}
public function acties(): HasMany
{
return $this->hasMany(Actie::class, 'eigenaar_id');
}
public function documents(): HasMany
{
return $this->hasMany(Document::class, 'auteur_id');
}
public function kennisArtikelen(): HasMany
{
return $this->hasMany(KennisArtikel::class, 'auteur_id');
}
public function auditLogs(): HasMany
{
return $this->hasMany(AuditLog::class);
}
}

View File

@@ -11,7 +11,9 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
$middleware->web(append: [
\App\Http\Middleware\HandleInertiaRequests::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//

View File

@@ -7,6 +7,7 @@
"license": "MIT",
"require": {
"php": "^8.3",
"inertiajs/inertia-laravel": "^3.0",
"laravel/framework": "^13.0",
"laravel/tinker": "^3.0"
},

75
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c57754c93ae34ac3b9b716a0fd2f2149",
"content-hash": "1c6dc1e0948ef3b8fd1905a824a40c58",
"packages": [
{
"name": "brick/math",
@@ -1053,6 +1053,79 @@
],
"time": "2025-08-22T14:27:06+00:00"
},
{
"name": "inertiajs/inertia-laravel",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/inertiajs/inertia-laravel.git",
"reference": "4675331c428c0f77b2539684835c5e0fd27ee023"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/4675331c428c0f77b2539684835c5e0fd27ee023",
"reference": "4675331c428c0f77b2539684835c5e0fd27ee023",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": "^11.0|^12.0|^13.0",
"php": "^8.2.0",
"symfony/console": "^7.0|^8.0"
},
"conflict": {
"laravel/boost": "<2.2.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.2",
"larastan/larastan": "^3.0",
"laravel/pint": "^1.16",
"mockery/mockery": "^1.3.3",
"orchestra/testbench": "^9.2|^10.0|^11.0",
"phpunit/phpunit": "^11.5|^12.0",
"roave/security-advisories": "dev-master"
},
"suggest": {
"ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command."
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Inertia\\ServiceProvider"
]
}
},
"autoload": {
"files": [
"./helpers.php"
],
"psr-4": {
"Inertia\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jonathan Reinink",
"email": "jonathan@reinink.ca",
"homepage": "https://reinink.ca"
}
],
"description": "The Laravel adapter for Inertia.js.",
"keywords": [
"inertia",
"laravel"
],
"support": {
"issues": "https://github.com/inertiajs/inertia-laravel/issues",
"source": "https://github.com/inertiajs/inertia-laravel/tree/v3.0.1"
},
"time": "2026-03-25T21:07:46+00:00"
},
{
"name": "laravel/framework",
"version": "v13.2.0",

View File

@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('functie')->nullable()->after('name');
$table->string('afdeling')->nullable()->after('functie');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['functie', 'afdeling']);
});
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('naam');
$table->string('beschrijving')->nullable();
$table->json('permissies')->nullable();
$table->timestamps();
});
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('role_id')->constrained()->cascadeOnDelete();
$table->primary(['user_id', 'role_id']);
});
}
public function down(): void
{
Schema::dropIfExists('role_user');
Schema::dropIfExists('roles');
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('themas', function (Blueprint $table) {
$table->id();
$table->string('naam');
$table->text('beschrijving');
$table->enum('prioriteit', ['laag', 'midden', 'hoog'])->default('midden');
$table->date('periode_start')->nullable();
$table->date('periode_eind')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('themas');
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('speerpunten', function (Blueprint $table) {
$table->id();
$table->foreignId('thema_id')->constrained('themas')->restrictOnDelete();
$table->string('naam');
$table->text('beschrijving');
$table->foreignId('eigenaar_id')->constrained('users')->restrictOnDelete();
$table->enum('status', ['concept', 'actief', 'afgerond'])->default('concept');
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('speerpunten');
}
};

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->foreignId('speerpunt_id')->nullable()->constrained('speerpunten')->nullOnDelete();
$table->string('naam');
$table->text('beschrijving');
$table->foreignId('eigenaar_id')->constrained('users')->restrictOnDelete();
$table->enum('status', [
'signaal',
'verkenning',
'concept',
'experiment',
'pilot',
'besluitvorming',
'overdracht_bouwen',
'overdracht_beheer',
'evaluatie',
'geparkeerd',
'gestopt',
'afgerond',
])->default('signaal');
$table->enum('prioriteit', ['laag', 'midden', 'hoog'])->default('midden');
$table->date('startdatum')->nullable();
$table->date('streef_einddatum')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('project_user', function (Blueprint $table) {
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->enum('rol', ['eigenaar', 'lid', 'reviewer', 'stakeholder'])->default('lid');
$table->timestamps();
$table->primary(['project_id', 'user_id']);
});
}
public function down(): void
{
Schema::dropIfExists('project_user');
Schema::dropIfExists('projects');
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('fases', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->enum('type', [
'signaal',
'verkenning',
'concept',
'experiment',
'pilot',
'besluitvorming',
'overdracht_bouwen',
'overdracht_beheer',
'evaluatie',
]);
$table->enum('status', ['open', 'actief', 'afgerond'])->default('open');
$table->date('startdatum')->nullable();
$table->date('einddatum')->nullable();
$table->text('opmerkingen')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('fases');
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('risicos', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->text('beschrijving');
$table->enum('impact', ['laag', 'midden', 'hoog'])->default('midden');
$table->enum('kans', ['laag', 'midden', 'hoog'])->default('midden');
$table->text('mitigatie')->nullable();
$table->foreignId('eigenaar_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('risicos');
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('afhankelijkheden', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->foreignId('afhankelijk_van_project_id')->constrained('projects')->restrictOnDelete();
$table->string('type');
$table->text('beschrijving')->nullable();
$table->enum('status', ['open', 'opgelost'])->default('open');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('afhankelijkheden');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('besluiten', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->string('titel');
$table->text('beschrijving');
$table->date('datum');
$table->string('type');
$table->enum('status', ['concept', 'voorgelegd', 'goedgekeurd', 'afgewezen'])->default('concept');
$table->text('onderbouwing')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('besluiten');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('commitments', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->nullable()->constrained('projects')->nullOnDelete();
$table->foreignId('besluit_id')->nullable()->constrained('besluiten')->nullOnDelete();
$table->text('beschrijving');
$table->foreignId('eigenaar_id')->constrained('users')->restrictOnDelete();
$table->date('deadline');
$table->enum('status', ['open', 'in_uitvoering', 'afgerond', 'verlopen'])->default('open');
$table->string('bron')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('commitments');
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('acties', function (Blueprint $table) {
$table->id();
$table->foreignId('commitment_id')->constrained('commitments')->cascadeOnDelete();
$table->text('beschrijving');
$table->foreignId('eigenaar_id')->constrained('users')->restrictOnDelete();
$table->date('deadline')->nullable();
$table->enum('status', ['open', 'in_uitvoering', 'afgerond'])->default('open');
$table->enum('prioriteit', ['laag', 'midden', 'hoog'])->default('midden');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('acties');
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('budgets', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->decimal('bedrag', 12, 2);
$table->string('type');
$table->string('periode')->nullable();
$table->enum('status', ['aangevraagd', 'toegekend', 'uitgeput'])->default('aangevraagd');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('budgets');
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('bestedingen', function (Blueprint $table) {
$table->id();
$table->foreignId('budget_id')->constrained('budgets')->cascadeOnDelete();
$table->decimal('bedrag', 12, 2);
$table->text('beschrijving');
$table->date('datum');
$table->string('categorie')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('bestedingen');
}
};

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->nullable()->constrained('projects')->nullOnDelete();
$table->foreignId('fase_id')->nullable()->constrained('fases')->nullOnDelete();
$table->string('titel');
$table->string('type');
$table->text('inhoud')->nullable();
$table->string('bestandspad')->nullable();
$table->integer('versie')->default(1);
$table->foreignId('auteur_id')->constrained('users')->restrictOnDelete();
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('documents');
}
};

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('naam');
$table->string('categorie')->nullable();
$table->timestamps();
});
Schema::create('document_tag', function (Blueprint $table) {
$table->foreignId('document_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->primary(['document_id', 'tag_id']);
});
}
public function down(): void
{
Schema::dropIfExists('document_tag');
Schema::dropIfExists('tags');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('kennis_artikelen', function (Blueprint $table) {
$table->id();
$table->string('titel');
$table->text('inhoud');
$table->foreignId('auteur_id')->constrained('users')->restrictOnDelete();
$table->timestamps();
$table->softDeletes();
});
Schema::create('kennis_artikel_tag', function (Blueprint $table) {
$table->foreignId('kennis_artikel_id')->constrained('kennis_artikelen')->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->primary(['kennis_artikel_id', 'tag_id']);
});
}
public function down(): void
{
Schema::dropIfExists('kennis_artikel_tag');
Schema::dropIfExists('kennis_artikelen');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('lessons_learned', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->nullable()->constrained('projects')->nullOnDelete();
$table->foreignId('fase_id')->nullable()->constrained('fases')->nullOnDelete();
$table->string('titel');
$table->text('inhoud');
$table->timestamps();
});
Schema::create('lesson_learned_tag', function (Blueprint $table) {
$table->foreignId('lesson_learned_id')->constrained('lessons_learned')->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->primary(['lesson_learned_id', 'tag_id']);
});
}
public function down(): void
{
Schema::dropIfExists('lesson_learned_tag');
Schema::dropIfExists('lessons_learned');
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('overdrachtsplannen', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->enum('type', ['naar_bouwen', 'naar_beheer']);
$table->enum('status', ['concept', 'in_uitvoering', 'afgerond'])->default('concept');
$table->foreignId('eigenaar_rnd_id')->constrained('users')->restrictOnDelete();
$table->foreignId('eigenaar_ontvanger_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('overdrachtsplannen');
}
};

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('criteria', function (Blueprint $table) {
$table->id();
$table->foreignId('overdrachtsplan_id')->constrained('overdrachtsplannen')->cascadeOnDelete();
$table->text('beschrijving');
$table->enum('status', ['open', 'voldaan', 'niet_voldaan'])->default('open');
$table->text('verificatie')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('criteria');
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('acceptaties', function (Blueprint $table) {
$table->id();
$table->foreignId('overdrachtsplan_id')->constrained('overdrachtsplannen')->cascadeOnDelete();
$table->date('datum');
$table->foreignId('door_id')->constrained('users')->restrictOnDelete();
$table->text('opmerkingen')->nullable();
$table->enum('status', ['geaccepteerd', 'afgewezen', 'voorwaardelijk']);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('acceptaties');
}
};

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('roadmap_items', function (Blueprint $table) {
$table->id();
$table->foreignId('thema_id')->constrained('themas')->cascadeOnDelete();
$table->string('titel');
$table->date('start');
$table->date('eind');
$table->string('type');
$table->string('status');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('roadmap_items');
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('audit_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('action');
$table->string('entity_type');
$table->unsignedBigInteger('entity_id')->nullable();
$table->json('payload')->nullable();
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void
{
Schema::dropIfExists('audit_logs');
}
};

152
docker-compose.yml Normal file
View File

@@ -0,0 +1,152 @@
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- .:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- laravel-app
networks:
- innovatieplatform
laravel-app:
build:
context: .
dockerfile: docker/php/Dockerfile
volumes:
- .:/var/www/html
environment:
- APP_ENV=${APP_ENV:-local}
- APP_KEY=${APP_KEY}
- DB_CONNECTION=pgsql
- DB_HOST=postgresql
- DB_PORT=5432
- DB_DATABASE=${DB_DATABASE:-innovatieplatform}
- DB_USERNAME=${DB_USERNAME:-innovatie}
- DB_PASSWORD=${DB_PASSWORD:-secret}
- REDIS_HOST=redis
- REDIS_PORT=6379
- AI_SERVICE_URL=http://ai-service:8000
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- innovatieplatform
laravel-worker:
build:
context: .
dockerfile: docker/php/Dockerfile
command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600
volumes:
- .:/var/www/html
environment:
- APP_ENV=${APP_ENV:-local}
- APP_KEY=${APP_KEY}
- DB_CONNECTION=pgsql
- DB_HOST=postgresql
- DB_PORT=5432
- DB_DATABASE=${DB_DATABASE:-innovatieplatform}
- DB_USERNAME=${DB_USERNAME:-innovatie}
- DB_PASSWORD=${DB_PASSWORD:-secret}
- REDIS_HOST=redis
- REDIS_PORT=6379
- AI_SERVICE_URL=http://ai-service:8000
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- innovatieplatform
laravel-scheduler:
build:
context: .
dockerfile: docker/php/Dockerfile
entrypoint: ["/usr/local/bin/scheduler-entrypoint.sh"]
volumes:
- .:/var/www/html
- ./docker/scheduler/entrypoint.sh:/usr/local/bin/scheduler-entrypoint.sh
environment:
- APP_ENV=${APP_ENV:-local}
- APP_KEY=${APP_KEY}
- DB_CONNECTION=pgsql
- DB_HOST=postgresql
- DB_PORT=5432
- DB_DATABASE=${DB_DATABASE:-innovatieplatform}
- DB_USERNAME=${DB_USERNAME:-innovatie}
- DB_PASSWORD=${DB_PASSWORD:-secret}
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- innovatieplatform
postgresql:
image: pgvector/pgvector:pg16
ports:
- "5432:5432"
environment:
- POSTGRES_DB=${DB_DATABASE:-innovatieplatform}
- POSTGRES_USER=${DB_USERNAME:-innovatie}
- POSTGRES_PASSWORD=${DB_PASSWORD:-secret}
volumes:
- postgresql_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-innovatie} -d ${DB_DATABASE:-innovatieplatform}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- innovatieplatform
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- innovatieplatform
ai-service:
build:
context: ./ai-service
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./ai-service:/app
environment:
- DB_HOST=postgresql
- DB_PORT=5432
- DB_DATABASE=${DB_DATABASE:-innovatieplatform}
- DB_USERNAME=${DB_USERNAME:-innovatie}
- DB_PASSWORD=${DB_PASSWORD:-secret}
depends_on:
postgresql:
condition: service_healthy
networks:
- innovatieplatform
volumes:
postgresql_data:
redis_data:
networks:
innovatieplatform:
driver: bridge

46
docker/nginx/default.conf Normal file
View File

@@ -0,0 +1,46 @@
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php index.html;
charset utf-8;
# Handle static files directly
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ {
expires max;
log_not_found off;
access_log off;
}
# Laravel routing — try files, then directories, then pass to index.php
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; }
# PHP-FPM — pass .php requests to laravel-app
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass laravel-app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 300;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
}
# Deny access to hidden files
location ~ /\.(?!well-known).* {
deny all;
}
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
}

53
docker/php/Dockerfile Normal file
View File

@@ -0,0 +1,53 @@
FROM php:8.4-fpm
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libzip-dev \
libicu-dev \
libpq-dev \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Configure and install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
pdo_pgsql \
pgsql \
pcntl \
bcmath \
gd \
zip \
intl
# Install Redis extension via PECL
RUN pecl install redis \
&& docker-php-ext-enable redis
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application files
COPY . .
# Install PHP dependencies (production-ready; skip dev deps when APP_ENV=production)
RUN composer install --no-interaction --prefer-dist --optimize-autoloader
# Set ownership to www-data
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage \
&& chmod -R 755 /var/www/html/bootstrap/cache
# Switch to non-root user
USER www-data
EXPOSE 9000
CMD ["php-fpm"]

View File

@@ -0,0 +1,8 @@
#!/bin/sh
echo "Starting Laravel Scheduler..."
while true; do
php /var/www/html/artisan schedule:run --verbose --no-interaction
sleep 60
done

2322
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,18 @@
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@tailwindcss/vite": "^4.2.2",
"axios": ">=1.11.0 <=1.14.0",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^3.0.0",
"tailwindcss": "^4.0.0",
"tailwindcss": "^4.2.2",
"vite": "^8.0.0"
},
"dependencies": {
"@inertiajs/vue3": "^3.0.1",
"@vitejs/plugin-vue": "^6.0.5",
"@vueuse/core": "^14.2.1",
"pinia": "^3.0.4",
"vue": "^3.5.31"
}
}

View File

@@ -1,11 +1 @@
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
@import "tailwindcss";

View File

@@ -0,0 +1,25 @@
<script setup>
import { Link } from '@inertiajs/vue3'
</script>
<template>
<div class="min-h-screen bg-gray-50">
<nav class="bg-white border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<span class="text-xl font-semibold text-gray-800">Innovatieplatform</span>
</div>
<div class="flex items-center space-x-4">
<Link href="/dashboard" class="text-gray-600 hover:text-gray-900">Dashboard</Link>
<Link href="/projects" class="text-gray-600 hover:text-gray-900">Projecten</Link>
<Link href="/roadmap" class="text-gray-600 hover:text-gray-900">Roadmap</Link>
</div>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<slot />
</main>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
</script>
<template>
<AppLayout>
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
<p class="mt-2 text-gray-600">Welkom bij het Innovatieplatform van het R&amp;D Lab.</p>
</AppLayout>
</template>

View File

@@ -1 +1,17 @@
import './bootstrap';
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { createPinia } from 'pinia'
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
return pages[`./Pages/${name}.vue`]
},
setup({ el, App, props, plugin }) {
const pinia = createPinia()
createApp({ render: () => h(App, props) })
.use(plugin)
.use(pinia)
.mount(el)
},
})

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Innovatieplatform</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@inertiaHead
</head>
<body>
@inertia
</body>
</html>

View File

@@ -1,7 +1,12 @@
<?php
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('/', function () {
return view('welcome');
return Inertia::render('Dashboard');
});
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
});

View File

@@ -1,6 +1,8 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import { resolve } from 'path'
export default defineConfig({
plugins: [
@@ -8,11 +10,17 @@ export default defineConfig({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
vue(),
tailwindcss(),
],
resolve: {
alias: {
'@': resolve(__dirname, 'resources/js'),
},
},
server: {
watch: {
ignored: ['**/storage/framework/views/**'],
},
},
});
})