Initial scaffold : Laravel 12 + PostgreSQL + auth + domaine métier (étapes 1-5)

- Laravel 12 sur PHP 8.5, Breeze (Blade/Tailwind/Alpine.js)
- Docker Compose dev (PostgreSQL 18 + Redis) et prod (stack complète + nginx)
- Migrations et models : lieux, sections, dépôts, source_types/fields, sources, relevés
  - Colonne JSONB data sur releves avec colonnes générées indexées (nom, prenom, date_evenement)
  - Index GIN pour la recherche fulltext
- Enums : UserRole, SourceStatus (avec transitions), CalendarType, FieldType
- RoleMiddleware (alias `role`) + helpers isAdmin/isSectionManager sur User
- CRUD Lieux (arbre hiérarchique, calcul nom_long en cascade)
- CRUD admin : Sections (+ gestion membres), Dépôts, Types de sources (+ champs dynamiques, drag & drop)
- CRUD Sources : visibilité filtrée par rôle, assignation membres, workflow de statut

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 16:16:37 +02:00
commit 7609d35287
172 changed files with 19179 additions and 0 deletions
@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('role')->default('member'); // admin | section_manager | member
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
@@ -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('lieux', function (Blueprint $table) {
$table->id();
$table->string('nom');
$table->string('code')->nullable();
$table->foreignId('lieu_parent_id')->nullable()->constrained('lieux')->nullOnDelete();
$table->string('nom_long')->nullable(); // calculé : "Bordeaux, Gironde, France"
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->text('note')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('lieux');
}
};
@@ -0,0 +1,34 @@
<?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('sections', function (Blueprint $table) {
$table->id();
$table->string('nom');
$table->foreignId('lieu_id')->nullable()->constrained('lieux')->nullOnDelete();
$table->string('adresse')->nullable();
$table->string('email_contact')->nullable();
$table->string('url')->nullable();
$table->timestamps();
});
Schema::create('section_user', function (Blueprint $table) {
$table->foreignId('section_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('role_in_section')->default('member'); // section_manager | member
$table->primary(['section_id', 'user_id']);
});
}
public function down(): void
{
Schema::dropIfExists('section_user');
Schema::dropIfExists('sections');
}
};
@@ -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('depots', function (Blueprint $table) {
$table->id();
$table->string('nom');
$table->text('description')->nullable();
$table->string('adresse_postale')->nullable();
$table->string('url')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('depots');
}
};
@@ -0,0 +1,38 @@
<?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('source_types', function (Blueprint $table) {
$table->id();
$table->string('nom');
$table->text('description')->nullable();
$table->timestamps();
});
Schema::create('source_type_fields', function (Blueprint $table) {
$table->id();
$table->foreignId('source_type_id')->constrained()->cascadeOnDelete();
$table->string('name'); // clé JSON (ex: "nom_pere")
$table->string('label'); // libellé affiché
$table->string('type'); // FieldType enum
$table->boolean('required')->default(false);
$table->unsignedSmallInteger('order')->default(0);
$table->json('options')->nullable(); // pour type=select : liste des choix
$table->timestamps();
$table->unique(['source_type_id', 'name']);
});
}
public function down(): void
{
Schema::dropIfExists('source_type_fields');
Schema::dropIfExists('source_types');
}
};
@@ -0,0 +1,35 @@
<?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('sources', function (Blueprint $table) {
$table->id();
$table->string('nom');
$table->text('description')->nullable();
$table->foreignId('source_type_id')->constrained()->restrictOnDelete();
$table->foreignId('depot_id')->nullable()->constrained('depots')->nullOnDelete();
$table->string('cote')->nullable();
$table->string('auteur')->nullable();
$table->string('status')->default('a_faire'); // SourceStatus enum
$table->timestamps();
});
Schema::create('source_user', function (Blueprint $table) {
$table->foreignId('source_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->primary(['source_id', 'user_id']);
});
}
public function down(): void
{
Schema::dropIfExists('source_user');
Schema::dropIfExists('sources');
}
};
@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
Schema::create('releves', function (Blueprint $table) {
$table->id();
$table->foreignId('source_id')->constrained()->cascadeOnDelete();
$table->jsonb('data'); // champs variables selon source_type
$table->foreignId('created_by')->constrained('users')->restrictOnDelete();
$table->foreignId('updated_by')->constrained('users')->restrictOnDelete();
$table->timestamps();
});
// Colonnes générées stockées pour les champs les plus filtrés
DB::statement("ALTER TABLE releves ADD COLUMN nom TEXT GENERATED ALWAYS AS (data->>'nom') STORED");
DB::statement("ALTER TABLE releves ADD COLUMN prenom TEXT GENERATED ALWAYS AS (data->>'prenom') STORED");
DB::statement("ALTER TABLE releves ADD COLUMN date_evenement TEXT GENERATED ALWAYS AS (data->'date_evenement'->>'valeur') STORED");
// Index pour les colonnes générées
DB::statement('CREATE INDEX releves_nom_idx ON releves (nom)');
DB::statement('CREATE INDEX releves_prenom_idx ON releves (prenom)');
DB::statement('CREATE INDEX releves_date_evenement_idx ON releves (date_evenement)');
// Index GIN pour la recherche fulltext sur l'ensemble du JSONB
DB::statement('CREATE INDEX releves_data_gin_idx ON releves USING gin (data)');
}
public function down(): void
{
Schema::dropIfExists('releves');
}
};