- Story 1.1: Permission enum, config, AuthorizesPermissions & HasWorkspaceScope traits, member→worker migration - Story 1.2: Team page with member list, invitation system with queued email - Story 1.3: Role assignment (Manager/Worker) and member removal with activity logging - Story 1.4: Owner-only permission toggle matrix for Managers (manage team, view logs, configure portal) - Story 1.5: Role-based access enforcement — Workers see only assigned declarations/clients, sidebar scoping - Story 1.6: Workspace switcher dropdown for multi-workspace users with session-based switching - 83 new/modified files, 182 tests passing with zero regressions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
338 lines
10 KiB
PHP
338 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Concerns\HasWorkspaceScope;
|
|
use App\Enums\ClientStatus;
|
|
use App\Enums\LegalForm;
|
|
use App\Enums\WorkspaceUserRole;
|
|
use App\Http\Requests\StoreClientRequest;
|
|
use App\Http\Requests\UpdateClientRequest;
|
|
use App\Models\Client;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class ClientController extends Controller
|
|
{
|
|
use HasWorkspaceScope;
|
|
|
|
protected function legalFormLabels(): array
|
|
{
|
|
$labels = [
|
|
'sarl' => 'SARL',
|
|
'sa' => 'SA',
|
|
'snc' => 'SNC',
|
|
'scs' => 'SCS',
|
|
'eurl' => 'EURL',
|
|
'sel' => 'SEL',
|
|
'auto_entrepreneur' => 'Auto-entrepreneur',
|
|
'entreprise_individuelle' => 'Entreprise individuelle',
|
|
'other' => 'Autre',
|
|
];
|
|
|
|
return array_intersect_key($labels, array_flip(LegalForm::getValues()));
|
|
}
|
|
|
|
protected function clientStatusLabels(): array
|
|
{
|
|
return [
|
|
ClientStatus::Active => 'Actif',
|
|
ClientStatus::Inactive => 'Inactif',
|
|
ClientStatus::Suspended => 'Suspendu',
|
|
];
|
|
}
|
|
|
|
protected function serializeContacts(Client $client): array
|
|
{
|
|
return $client->contacts->map(fn ($c) => [
|
|
'id' => $c->id,
|
|
'full_name' => $c->full_name,
|
|
'job_title' => $c->job_title,
|
|
'email' => $c->email,
|
|
'phone' => $c->phone,
|
|
'is_principal' => $c->is_principal,
|
|
])->all();
|
|
}
|
|
|
|
protected function isWorker(): bool
|
|
{
|
|
return auth()->user()->currentWorkspaceUser()->role->is(WorkspaceUserRole::Worker);
|
|
}
|
|
|
|
/**
|
|
* Display a listing of the clients.
|
|
*/
|
|
public function index(Request $request): Response
|
|
{
|
|
$workspace = $this->currentWorkspace();
|
|
$user = auth()->user();
|
|
$isWorker = $this->isWorker();
|
|
|
|
$perPage = min(max((int) $request->input('per_page', 10), 10), 100);
|
|
|
|
$query = $workspace->clients();
|
|
|
|
if ($isWorker) {
|
|
$query->whereHas('declarations', fn ($q) => $q->where('assigned_to', $user->id));
|
|
}
|
|
|
|
$clients = $query
|
|
->latest()
|
|
->paginate($perPage)
|
|
->through(fn (Client $client) => [
|
|
'id' => $client->id,
|
|
'company_name' => $client->company_name,
|
|
'legal_form' => $client->legal_form->value,
|
|
'ice' => $client->ice,
|
|
'status' => $client->status?->value,
|
|
'showUrl' => route('clients.show', $client),
|
|
'editUrl' => route('clients.edit', $client),
|
|
'destroyUrl' => route('clients.destroy', $client),
|
|
]);
|
|
|
|
return Inertia::render('clients/Index', [
|
|
'clients' => $clients,
|
|
'createUrl' => route('clients.create'),
|
|
'workspaceName' => $workspace->name,
|
|
'canCreate' => ! $isWorker,
|
|
'canEdit' => ! $isWorker,
|
|
'canDelete' => ! $isWorker,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Show the form for creating a new client.
|
|
*/
|
|
public function create(Request $request): Response
|
|
{
|
|
if ($this->isWorker()) {
|
|
abort(404);
|
|
}
|
|
|
|
$workspace = $this->currentWorkspace();
|
|
|
|
return Inertia::render('clients/Create', [
|
|
'indexUrl' => route('clients.index'),
|
|
'storeUrl' => route('clients.store'),
|
|
'legalForms' => $this->legalFormLabels(),
|
|
'clientStatusLabels' => $this->clientStatusLabels(),
|
|
'workspaceUsers' => $workspace->users()->orderBy('users.name')->select('users.id', 'users.name', 'users.email')->get()->map(fn ($u) => [
|
|
'id' => $u->id,
|
|
'name' => $u->name,
|
|
'email' => $u->email,
|
|
])->values()->all(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Store a newly created client in storage.
|
|
*/
|
|
public function store(StoreClientRequest $request): RedirectResponse
|
|
{
|
|
if ($this->isWorker()) {
|
|
abort(404);
|
|
}
|
|
|
|
$workspace = $this->currentWorkspace();
|
|
$data = $request->validated();
|
|
$contacts = $data['contacts'];
|
|
unset($data['contacts']);
|
|
$data['workspace_id'] = $workspace->id;
|
|
|
|
$client = Client::query()->create($data);
|
|
|
|
foreach ($contacts as $contact) {
|
|
$client->contacts()->create($contact);
|
|
}
|
|
|
|
return to_route('clients.index');
|
|
}
|
|
|
|
/**
|
|
* Display the specified client.
|
|
*/
|
|
public function show(Request $request, Client $client): Response
|
|
{
|
|
$this->authorizeWorkspaceAccess($client);
|
|
|
|
$isWorker = $this->isWorker();
|
|
$user = auth()->user();
|
|
|
|
if ($isWorker) {
|
|
$hasAssignedDeclarations = $client->declarations()
|
|
->where('assigned_to', $user->id)
|
|
->exists();
|
|
|
|
if (! $hasAssignedDeclarations) {
|
|
abort(404);
|
|
}
|
|
}
|
|
|
|
$client->load(['internalResponsible', 'contacts']);
|
|
|
|
$declarationsQuery = $client->declarations();
|
|
|
|
if ($isWorker) {
|
|
$declarationsQuery->where('assigned_to', $user->id);
|
|
}
|
|
|
|
$declarations = (clone $declarationsQuery)
|
|
->with(['assignee'])
|
|
->latest()
|
|
->limit(50)
|
|
->get()
|
|
->map(fn ($f) => [
|
|
'id' => $f->id,
|
|
'title' => $f->title,
|
|
'type' => $f->type->value,
|
|
'status' => $f->status->value,
|
|
'due_date' => $f->due_date?->format('Y-m-d'),
|
|
'created_at' => $f->created_at->format('Y-m-d'),
|
|
'showUrl' => route('declarations.show', $f),
|
|
])
|
|
->values()
|
|
->all();
|
|
|
|
$allDeclarations = (clone $declarationsQuery)->get();
|
|
$stats = [
|
|
'total' => $allDeclarations->count(),
|
|
'by_status' => $allDeclarations->groupBy(fn ($f) => $f->status->value)
|
|
->map->count()
|
|
->all(),
|
|
'by_type' => $allDeclarations->groupBy(fn ($f) => $f->type->value)
|
|
->map->count()
|
|
->all(),
|
|
];
|
|
|
|
return Inertia::render('clients/Show', [
|
|
'client' => [
|
|
'id' => $client->id,
|
|
'company_name' => $client->company_name,
|
|
'legal_form' => $client->legal_form->value,
|
|
'ice' => $client->ice,
|
|
'fiscal_id' => $client->fiscal_id,
|
|
'rc' => $client->rc,
|
|
'cnss' => $client->cnss,
|
|
'patente' => $client->patente,
|
|
'contacts' => $this->serializeContacts($client),
|
|
'internal_responsible_id' => $client->internal_responsible_id,
|
|
'internal_responsible_name' => $client->internalResponsible?->name,
|
|
'status' => $client->status?->value,
|
|
'internal_notes' => $client->internal_notes,
|
|
],
|
|
'declarations' => $declarations,
|
|
'stats' => $stats,
|
|
'indexUrl' => route('clients.index'),
|
|
'editUrl' => route('clients.edit', $client),
|
|
'createDeclarationUrl' => route('declarations.create', ['client_id' => $client->id]),
|
|
'canEdit' => ! $isWorker,
|
|
'canDelete' => ! $isWorker,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Show the form for editing the specified client.
|
|
*/
|
|
public function edit(Request $request, Client $client): Response
|
|
{
|
|
if ($this->isWorker()) {
|
|
abort(404);
|
|
}
|
|
|
|
$this->authorizeWorkspaceAccess($client);
|
|
|
|
$workspace = $this->currentWorkspace();
|
|
|
|
$client->load('contacts');
|
|
|
|
return Inertia::render('clients/Edit', [
|
|
'client' => [
|
|
'id' => $client->id,
|
|
'company_name' => $client->company_name,
|
|
'legal_form' => $client->legal_form->value,
|
|
'ice' => $client->ice,
|
|
'fiscal_id' => $client->fiscal_id,
|
|
'rc' => $client->rc,
|
|
'cnss' => $client->cnss,
|
|
'patente' => $client->patente,
|
|
'contacts' => $this->serializeContacts($client),
|
|
'internal_responsible_id' => $client->internal_responsible_id,
|
|
'status' => $client->status?->value,
|
|
'internal_notes' => $client->internal_notes,
|
|
],
|
|
'indexUrl' => route('clients.index'),
|
|
'updateUrl' => route('clients.update', $client),
|
|
'legalForms' => $this->legalFormLabels(),
|
|
'clientStatusLabels' => $this->clientStatusLabels(),
|
|
'workspaceUsers' => $workspace->users()->orderBy('users.name')->select('users.id', 'users.name', 'users.email')->get()->map(fn ($u) => [
|
|
'id' => $u->id,
|
|
'name' => $u->name,
|
|
'email' => $u->email,
|
|
])->values()->all(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Update the specified client in storage.
|
|
*/
|
|
public function update(UpdateClientRequest $request, Client $client): RedirectResponse
|
|
{
|
|
if ($this->isWorker()) {
|
|
abort(404);
|
|
}
|
|
|
|
$this->authorizeWorkspaceAccess($client);
|
|
|
|
$data = $request->validated();
|
|
$contacts = $data['contacts'];
|
|
unset($data['contacts']);
|
|
|
|
DB::transaction(function () use ($client, $data, $contacts) {
|
|
$client->update($data);
|
|
|
|
$submittedIds = collect($contacts)
|
|
->pluck('id')
|
|
->filter()
|
|
->all();
|
|
|
|
$client->contacts()
|
|
->whereNotIn('id', $submittedIds)
|
|
->get()
|
|
->each
|
|
->delete();
|
|
|
|
foreach ($contacts as $contactData) {
|
|
if (! empty($contactData['id'])) {
|
|
$client->contacts()
|
|
->where('id', $contactData['id'])
|
|
->first()
|
|
?->update($contactData);
|
|
} else {
|
|
$client->contacts()->create($contactData);
|
|
}
|
|
}
|
|
});
|
|
|
|
return to_route('clients.index');
|
|
}
|
|
|
|
/**
|
|
* Remove the specified client from storage.
|
|
*/
|
|
public function destroy(Request $request, Client $client): RedirectResponse
|
|
{
|
|
if ($this->isWorker()) {
|
|
abort(404);
|
|
}
|
|
|
|
$this->authorizeWorkspaceAccess($client);
|
|
|
|
$client->delete();
|
|
|
|
return to_route('clients.index');
|
|
}
|
|
}
|