feat: complete Epic 1 — team management & permission system
- 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>
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
|
||||
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 App\Models\Workspace;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -16,6 +17,8 @@ use Inertia\Response;
|
||||
|
||||
class ClientController extends Controller
|
||||
{
|
||||
use HasWorkspaceScope;
|
||||
|
||||
protected function legalFormLabels(): array
|
||||
{
|
||||
$labels = [
|
||||
@@ -42,13 +45,6 @@ class ClientController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
protected function currentWorkspace(Request $request): Workspace
|
||||
{
|
||||
$workspaceId = $request->session()->get('current_workspace_id');
|
||||
|
||||
return Workspace::query()->findOrFail($workspaceId);
|
||||
}
|
||||
|
||||
protected function serializeContacts(Client $client): array
|
||||
{
|
||||
return $client->contacts->map(fn ($c) => [
|
||||
@@ -61,16 +57,29 @@ class ClientController extends Controller
|
||||
])->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($request);
|
||||
$workspace = $this->currentWorkspace();
|
||||
$user = auth()->user();
|
||||
$isWorker = $this->isWorker();
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 10), 10), 100);
|
||||
|
||||
$clients = $workspace->clients()
|
||||
$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) => [
|
||||
@@ -88,6 +97,9 @@ class ClientController extends Controller
|
||||
'clients' => $clients,
|
||||
'createUrl' => route('clients.create'),
|
||||
'workspaceName' => $workspace->name,
|
||||
'canCreate' => ! $isWorker,
|
||||
'canEdit' => ! $isWorker,
|
||||
'canDelete' => ! $isWorker,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -96,7 +108,11 @@ class ClientController extends Controller
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
if ($this->isWorker()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$workspace = $this->currentWorkspace();
|
||||
|
||||
return Inertia::render('clients/Create', [
|
||||
'indexUrl' => route('clients.index'),
|
||||
@@ -116,7 +132,11 @@ class ClientController extends Controller
|
||||
*/
|
||||
public function store(StoreClientRequest $request): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
if ($this->isWorker()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$workspace = $this->currentWorkspace();
|
||||
$data = $request->validated();
|
||||
$contacts = $data['contacts'];
|
||||
unset($data['contacts']);
|
||||
@@ -136,12 +156,30 @@ class ClientController extends Controller
|
||||
*/
|
||||
public function show(Request $request, Client $client): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeClient($workspace, $client);
|
||||
$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']);
|
||||
|
||||
$declarations = $client->declarations()
|
||||
$declarationsQuery = $client->declarations();
|
||||
|
||||
if ($isWorker) {
|
||||
$declarationsQuery->where('assigned_to', $user->id);
|
||||
}
|
||||
|
||||
$declarations = (clone $declarationsQuery)
|
||||
->with(['assignee'])
|
||||
->latest()
|
||||
->limit(50)
|
||||
@@ -158,7 +196,7 @@ class ClientController extends Controller
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$allDeclarations = $client->declarations()->get();
|
||||
$allDeclarations = (clone $declarationsQuery)->get();
|
||||
$stats = [
|
||||
'total' => $allDeclarations->count(),
|
||||
'by_status' => $allDeclarations->groupBy(fn ($f) => $f->status->value)
|
||||
@@ -190,6 +228,8 @@ class ClientController extends Controller
|
||||
'indexUrl' => route('clients.index'),
|
||||
'editUrl' => route('clients.edit', $client),
|
||||
'createDeclarationUrl' => route('declarations.create', ['client_id' => $client->id]),
|
||||
'canEdit' => ! $isWorker,
|
||||
'canDelete' => ! $isWorker,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -198,8 +238,13 @@ class ClientController extends Controller
|
||||
*/
|
||||
public function edit(Request $request, Client $client): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeClient($workspace, $client);
|
||||
if ($this->isWorker()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->authorizeWorkspaceAccess($client);
|
||||
|
||||
$workspace = $this->currentWorkspace();
|
||||
|
||||
$client->load('contacts');
|
||||
|
||||
@@ -235,8 +280,11 @@ class ClientController extends Controller
|
||||
*/
|
||||
public function update(UpdateClientRequest $request, Client $client): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeClient($workspace, $client);
|
||||
if ($this->isWorker()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->authorizeWorkspaceAccess($client);
|
||||
|
||||
$data = $request->validated();
|
||||
$contacts = $data['contacts'];
|
||||
@@ -276,18 +324,14 @@ class ClientController extends Controller
|
||||
*/
|
||||
public function destroy(Request $request, Client $client): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeClient($workspace, $client);
|
||||
if ($this->isWorker()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->authorizeWorkspaceAccess($client);
|
||||
|
||||
$client->delete();
|
||||
|
||||
return to_route('clients.index');
|
||||
}
|
||||
|
||||
protected function authorizeClient(Workspace $workspace, Client $client): void
|
||||
{
|
||||
if ($client->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user