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:
2026-03-11 23:33:10 +00:00
commit 35545c2a8f
1517 changed files with 246774 additions and 0 deletions

View 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>