feat: L'Ami Fiduciaire V1.0.0 — full codebase with Story 0.1 complete
Initial commit of the L'Ami Fiduciaire SaaS platform built on Laravel 12, Vue 3, Inertia.js 2, and Tailwind CSS 4. Story 0.1 (rename folders to declarations in database) is implemented and code-reviewed: migration, rollback, and 6 Pest tests all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
160
resources/js/components/WorkspaceForm.vue
Normal file
160
resources/js/components/WorkspaceForm.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import type { Form } from '@inertiajs/vue3';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
|
||||
export type WorkspaceFormData = {
|
||||
name: string;
|
||||
slug: string;
|
||||
user_ids: number[];
|
||||
user_roles: Record<number, string>;
|
||||
};
|
||||
|
||||
export type WorkspaceFormUser = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
form: Form<WorkspaceFormData>;
|
||||
users: WorkspaceFormUser[];
|
||||
workspaceUserRoles: Record<string, string>;
|
||||
submitLabel?: string;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
submitLabel: 'Save',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [];
|
||||
}>();
|
||||
|
||||
const defaultRole = 'member';
|
||||
|
||||
function onUserToggle(userId: number, checked: boolean) {
|
||||
if (checked) {
|
||||
const ids = props.form.user_ids ?? [];
|
||||
if (!ids.includes(userId)) {
|
||||
props.form.user_ids = [...ids, userId];
|
||||
}
|
||||
const roles = props.form.user_roles ?? {};
|
||||
props.form.user_roles = { ...roles, [userId]: roles[userId] ?? defaultRole };
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function onRoleChange(userId: number, role: string) {
|
||||
const roles = props.form.user_roles ?? {};
|
||||
props.form.user_roles = { ...roles, [userId]: role };
|
||||
}
|
||||
|
||||
function isUserSelected(userId: number): boolean {
|
||||
return (props.form.user_ids ?? []).includes(userId);
|
||||
}
|
||||
|
||||
function getUserRole(userId: number): string {
|
||||
return props.form.user_roles?.[userId] ?? defaultRole;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="emit('submit')" class="flex flex-col space-y-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Cabinet Comptable XYZ"
|
||||
aria-invalid="!!form.errors.name"
|
||||
/>
|
||||
<InputError :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="slug">Slug</Label>
|
||||
<Input
|
||||
id="slug"
|
||||
v-model="form.slug"
|
||||
type="text"
|
||||
placeholder="cabinet-comptable-xyz (optional)"
|
||||
aria-invalid="!!form.errors.slug"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Leave empty to auto-generate from the name.
|
||||
</p>
|
||||
<InputError :message="form.errors.slug" />
|
||||
</div>
|
||||
|
||||
<div v-if="users?.length" class="grid gap-2">
|
||||
<Label>Users</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Select users to add to this workspace.
|
||||
</p>
|
||||
<div
|
||||
class="max-h-48 space-y-2 overflow-y-auto rounded-md border border-sidebar-border/70 p-3 dark:border-sidebar-border"
|
||||
>
|
||||
<div
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
class="flex items-center gap-3 rounded px-2 py-1.5 hover:bg-muted/50"
|
||||
>
|
||||
<input
|
||||
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)"
|
||||
/>
|
||||
<div class="min-w-0 flex-1 flex flex-col">
|
||||
<span class="text-sm font-medium">{{ user.name }}</span>
|
||||
<span class="text-xs text-muted-foreground">{{
|
||||
user.email
|
||||
}}</span>
|
||||
</div>
|
||||
<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"
|
||||
@change="
|
||||
onRoleChange(
|
||||
user.id,
|
||||
($event.target as HTMLSelectElement).value,
|
||||
)
|
||||
"
|
||||
>
|
||||
<option
|
||||
v-for="(label, value) in workspaceUserRoles"
|
||||
:key="value"
|
||||
:value="value"
|
||||
>
|
||||
{{ label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<InputError :message="form.errors.user_ids" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
data-test="workspace-form-submit"
|
||||
>
|
||||
<Spinner v-if="form.processing" />
|
||||
{{ submitLabel }}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
Reference in New Issue
Block a user