diff --git a/app/Models/Document.php b/app/Models/Document.php index 4fced43..ab5dec5 100644 --- a/app/Models/Document.php +++ b/app/Models/Document.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use League\CommonMark\CommonMarkConverter; -#[Fillable(['organization_id', 'folder_id', 'name', 'type', 'is_folder', 'content', 'file_path', 'office_extension', 'office_key', 'current_version', 'created_by'])] +#[Fillable(['organization_id', 'folder_id', 'location_id', 'name', 'type', 'is_folder', 'content', 'file_path', 'office_extension', 'office_key', 'current_version', 'created_by'])] class Document extends Model implements Linkable { use SoftDeletes; @@ -60,6 +60,11 @@ class Document extends Model implements Linkable return $this->belongsTo(Document::class, 'folder_id'); } + public function location() + { + return $this->belongsTo(Location::class); + } + public function children() { return $this->hasMany(Document::class, 'folder_id'); diff --git a/database/migrations/2026_08_24_090000_add_location_id_to_documents_table.php b/database/migrations/2026_08_24_090000_add_location_id_to_documents_table.php new file mode 100644 index 0000000..f2df5f5 --- /dev/null +++ b/database/migrations/2026_08_24_090000_add_location_id_to_documents_table.php @@ -0,0 +1,22 @@ +foreignId('location_id')->nullable()->after('folder_id')->constrained()->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('documents', function (Blueprint $table) { + $table->dropConstrainedForeignId('location_id'); + }); + } +}; diff --git a/resources/views/components/documents/⚡diagram-editor.blade.php b/resources/views/components/documents/⚡diagram-editor.blade.php index 2daf51d..a613b67 100644 --- a/resources/views/components/documents/⚡diagram-editor.blade.php +++ b/resources/views/components/documents/⚡diagram-editor.blade.php @@ -9,17 +9,19 @@ new class extends Component public Organization $organization; public Document $document; public string $backRoute = 'organizations.documents.index'; + public ?int $folderId = null; public bool $renaming = false; public string $name = ''; - public function mount(Organization $organization, Document $document, string $backRoute = 'organizations.documents.index'): void + public function mount(Organization $organization, Document $document, string $backRoute = 'organizations.documents.index', ?int $folderId = null): void { abort_unless($document->isDiagram(), 404); $this->organization = $organization; $this->document = $document; $this->backRoute = $backRoute; + $this->folderId = $folderId; $this->name = $document->name; } @@ -31,6 +33,11 @@ new class extends Component $this->renaming = false; } + public function updateLocation(?int $locationId): void + { + $this->document->update(['location_id' => $locationId]); + } + public function saveDiagram(string $xml): void { $contentChanged = $this->document->content !== $xml; @@ -46,6 +53,13 @@ new class extends Component ]); } } + + public function with(): array + { + return [ + 'locations' => $this->organization->locations()->orderBy('name')->get(), + ]; + } }; ?> @@ -66,7 +80,18 @@ new class extends Component Renommer @endif - Retour + Retour + + +
+ +
validate(['newFolderName' => 'required|string|max:255']); + + $this->organization->documents()->create([ + 'folder_id' => $this->folderId, + 'name' => $this->newFolderName, + 'type' => 'diagram', + 'is_folder' => true, + 'created_by' => auth()->id(), + ]); + + $this->newFolderName = ''; + $this->showNewFolder = false; + } + public function createDiagram(): void { $this->validate(['newDiagramName' => 'required|string|max:255']); @@ -29,6 +55,7 @@ new class extends Component XML; $document = $this->organization->documents()->create([ + 'folder_id' => $this->folderId, 'name' => $this->newDiagramName, 'type' => 'diagram', 'content' => $emptyDiagram, @@ -42,7 +69,7 @@ new class extends Component 'created_by' => auth()->id(), ]); - $this->redirectRoute('organizations.infrastructure.edit', [$this->organization, $document], navigate: true); + $this->redirectRoute('organizations.infrastructure.edit', [$this->organization, $document, 'folderId' => $this->folderId], navigate: true); } public function delete(int $documentId): void @@ -50,15 +77,49 @@ new class extends Component $document = $this->organization->documents()->where('type', 'diagram')->findOrFail($documentId); $document->delete(); - session()->flash('status', "Schéma « {$document->name} » supprimé."); + session()->flash('status', "« {$document->name} » supprimé."); + } + + private function folderTree(\Illuminate\Support\Collection $allFolders): \Illuminate\Support\Collection + { + $byParent = $allFolders->groupBy('folder_id'); + + $walk = function ($parentId, $depth) use (&$walk, $byParent) { + return ($byParent->get($parentId) ?? collect()) + ->sortBy('name') + ->flatMap(fn ($folder) => collect([['folder' => $folder, 'depth' => $depth]]) + ->concat($walk($folder->id, $depth + 1))); + }; + + return $walk(null, 0); } public function with(): array { + $currentFolder = $this->folderId + ? $this->organization->documents()->where('type', 'diagram')->find($this->folderId) + : null; + + $breadcrumbs = collect(); + $walk = $currentFolder; + while ($walk) { + $breadcrumbs->prepend($walk); + $walk = $walk->folder; + } + + $allFolders = $this->organization->documents()->where('type', 'diagram')->where('is_folder', true)->get(); + return [ - 'diagrams' => $this->organization->documents() + 'currentFolder' => $currentFolder, + 'breadcrumbs' => $breadcrumbs, + 'folderTree' => $this->folderTree($allFolders), + 'locations' => $this->organization->locations()->orderBy('name')->get(), + 'items' => $this->organization->documents() ->where('type', 'diagram') - ->with('creator') + ->where('folder_id', $this->folderId) + ->when($this->locationId, fn ($query) => $query->where('location_id', $this->locationId)) + ->with('location') + ->orderByDesc('is_folder') ->orderBy('name') ->get(), ]; @@ -69,10 +130,16 @@ new class extends Component

Infrastructure

- +
+ + +
@if (session('status')) @@ -81,6 +148,35 @@ new class extends Component
@endif +
+
+ Infrastructure + @foreach ($breadcrumbs as $crumb) + / + {{ $crumb->name }} + @endforeach +
+ + @if ($locations->isNotEmpty()) + + @endif +
+ + @if ($showNewFolder) +
+ + + + @error('newFolderName')

{{ $message }}

@enderror +
+ @endif + @if ($showNewDiagram)
@@ -99,26 +195,35 @@ new class extends Component Nom - Créé par + Site Mis à jour - @forelse ($diagrams as $diagram) - + @forelse ($items as $item) + @php $isFolder = $item->isFolder(); @endphp + - - 🗺️ {{ $diagram->name }} - + @if ($isFolder) + + 📁 {{ $item->name }} + + @else + + 🗺️ {{ $item->name }} + + @endif - {{ $diagram->creator?->name ?? '—' }} - {{ $diagram->updated_at->diffForHumans() }} + {{ $isFolder ? '—' : ($item->location?->name ?? '—') }} + {{ $item->updated_at->diffForHumans() }} - Ouvrir + @unless ($isFolder) + Ouvrir + @endunless @@ -126,7 +231,7 @@ new class extends Component @empty - Aucun schéma pour le moment. + Dossier vide. @endforelse diff --git a/resources/views/infrastructure/edit.blade.php b/resources/views/infrastructure/edit.blade.php index 3faa34c..d9c4489 100644 --- a/resources/views/infrastructure/edit.blade.php +++ b/resources/views/infrastructure/edit.blade.php @@ -1,3 +1,4 @@ - + diff --git a/tests/Feature/InfrastructureTest.php b/tests/Feature/InfrastructureTest.php index 943cdc9..b0c9633 100644 --- a/tests/Feature/InfrastructureTest.php +++ b/tests/Feature/InfrastructureTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature; +use App\Models\Location; use App\Models\Organization; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -100,4 +101,62 @@ class InfrastructureTest extends TestCase ->assertDontSee('Schéma réseau - Mairie') ->assertSee('Compte-rendu.md'); } + + public function test_can_create_a_folder_and_a_diagram_inside_it(): void + { + $user = User::factory()->create(); + $org = Organization::create(['name' => 'Test Commune', 'slug' => 'test-commune', 'org_type' => 'client']); + + $this->actingAs($user); + + $component = Livewire::test('infrastructure.index', ['organization' => $org]) + ->set('newFolderName', 'Réseau') + ->call('createFolder') + ->assertHasNoErrors(); + + $folder = $org->documents()->where('type', 'diagram')->where('is_folder', true)->firstOrFail(); + $this->assertSame('Réseau', $folder->name); + + $component->set('folderId', $folder->id) + ->set('newDiagramName', 'Schéma coeur de réseau') + ->call('createDiagram') + ->assertHasNoErrors(); + + $diagram = $org->documents()->where('type', 'diagram')->where('name', 'Schéma coeur de réseau')->firstOrFail(); + $this->assertSame($folder->id, $diagram->folder_id); + + // The folder itself must not leak into the general Documents browser. + Livewire::test('documents.index', ['organization' => $org])->assertDontSee('Réseau'); + } + + public function test_a_diagram_can_be_associated_with_a_site(): void + { + $user = User::factory()->create(); + $org = Organization::create(['name' => 'Test Commune', 'slug' => 'test-commune', 'org_type' => 'client']); + $location = Location::create(['organization_id' => $org->id, 'name' => 'Mairie']); + + $document = $org->documents()->create([ + 'name' => 'Schéma test', + 'type' => 'diagram', + 'content' => '', + 'current_version' => 1, + 'created_by' => $user->id, + ]); + + $this->actingAs($user); + + Livewire::test('documents.diagram-editor', ['organization' => $org, 'document' => $document]) + ->call('updateLocation', $location->id) + ->assertHasNoErrors(); + + $this->assertSame($location->id, $document->refresh()->location_id); + + Livewire::test('infrastructure.index', ['organization' => $org, 'locationId' => $location->id]) + ->assertSee('Schéma test'); + + $otherLocation = Location::create(['organization_id' => $org->id, 'name' => 'Annexe']); + + Livewire::test('infrastructure.index', ['organization' => $org, 'locationId' => $otherLocation->id]) + ->assertDontSee('Schéma test'); + } }