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:
2026-03-18 00:12:50 +00:00
parent 5dffd2d063
commit c89d1879bf
83 changed files with 5850 additions and 314 deletions

View File

@@ -34,7 +34,7 @@ const emit = defineEmits<{
submit: [];
}>();
const defaultRole = 'member';
const defaultRole = 'worker';
function onUserToggle(userId: number, checked: boolean) {
if (checked) {
@@ -43,9 +43,14 @@ function onUserToggle(userId: number, checked: boolean) {
props.form.user_ids = [...ids, userId];
}
const roles = props.form.user_roles ?? {};
props.form.user_roles = { ...roles, [userId]: roles[userId] ?? defaultRole };
props.form.user_roles = {
...roles,
[userId]: roles[userId] ?? defaultRole,
};
} else {
props.form.user_ids = (props.form.user_ids ?? []).filter((id) => id !== userId);
props.form.user_ids = (props.form.user_ids ?? []).filter(
(id) => id !== userId,
);
const roles = { ...(props.form.user_roles ?? {}) };
delete roles[userId];
props.form.user_roles = roles;
@@ -113,10 +118,15 @@ function getUserRole(userId: number): string {
type="checkbox"
:value="user.id"
:checked="isUserSelected(user.id)"
class="border-input size-4 shrink-0 rounded-[4px] border focus-visible:ring-2 focus-visible:ring-ring"
@change="onUserToggle(user.id, ($event.target as HTMLInputElement).checked)"
class="size-4 shrink-0 rounded-[4px] border border-input focus-visible:ring-2 focus-visible:ring-ring"
@change="
onUserToggle(
user.id,
($event.target as HTMLInputElement).checked,
)
"
/>
<div class="min-w-0 flex-1 flex flex-col">
<div class="flex min-w-0 flex-1 flex-col">
<span class="text-sm font-medium">{{ user.name }}</span>
<span class="text-xs text-muted-foreground">{{
user.email
@@ -125,7 +135,7 @@ function getUserRole(userId: number): string {
<select
:value="getUserRole(user.id)"
:disabled="!isUserSelected(user.id)"
class="border-input bg-background h-8 shrink-0 rounded-md border px-2 text-sm disabled:opacity-50"
class="h-8 shrink-0 rounded-md border border-input bg-background px-2 text-sm disabled:opacity-50"
@change="
onRoleChange(
user.id,