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:
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
import { CheckCircle, XCircle, X } from 'lucide-vue-next';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import AppContent from '@/components/AppContent.vue';
|
||||
import AppShell from '@/components/AppShell.vue';
|
||||
import AppSidebar from '@/components/AppSidebar.vue';
|
||||
@@ -12,6 +15,34 @@ type Props = {
|
||||
withDefaults(defineProps<Props>(), {
|
||||
breadcrumbs: () => [],
|
||||
});
|
||||
|
||||
const page = usePage<{
|
||||
flash: { success?: string; error?: string };
|
||||
}>();
|
||||
|
||||
const flashMessage = ref<{ type: 'success' | 'error'; text: string } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const flash = computed(() => page.props.flash);
|
||||
|
||||
watch(
|
||||
flash,
|
||||
(val) => {
|
||||
if (val?.success) {
|
||||
flashMessage.value = { type: 'success', text: val.success };
|
||||
setTimeout(() => {
|
||||
flashMessage.value = null;
|
||||
}, 4000);
|
||||
} else if (val?.error) {
|
||||
flashMessage.value = { type: 'error', text: val.error };
|
||||
setTimeout(() => {
|
||||
flashMessage.value = null;
|
||||
}, 4000);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -21,5 +52,40 @@ withDefaults(defineProps<Props>(), {
|
||||
<AppSidebarHeader :breadcrumbs="breadcrumbs" />
|
||||
<slot />
|
||||
</AppContent>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="transition duration-300 ease-out"
|
||||
enter-from-class="translate-y-2 opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition duration-200 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="translate-y-2 opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="flashMessage"
|
||||
class="fixed right-4 bottom-4 z-50 flex max-w-sm items-center gap-3 rounded-lg border bg-background px-4 py-3 shadow-lg"
|
||||
:class="
|
||||
flashMessage.type === 'success'
|
||||
? 'border-green-200 dark:border-green-800'
|
||||
: 'border-red-200 dark:border-red-800'
|
||||
"
|
||||
>
|
||||
<CheckCircle
|
||||
v-if="flashMessage.type === 'success'"
|
||||
class="h-5 w-5 shrink-0 text-green-600"
|
||||
/>
|
||||
<XCircle v-else class="h-5 w-5 shrink-0 text-red-600" />
|
||||
<span class="text-sm">{{ flashMessage.text }}</span>
|
||||
<button
|
||||
class="ml-auto shrink-0 text-muted-foreground hover:text-foreground"
|
||||
@click="flashMessage = null"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</AppShell>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user