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,309 @@
<script setup lang="ts">
import type { Form } from '@inertiajs/vue3';
import { watch } from 'vue';
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 FolderFormData = {
client_id: number | '';
title: string;
type: string;
period_year: number | string;
period_month: number | string;
period_quarter: number | string;
due_date: string;
status: string;
priority: string;
assigned_to: number | '';
notes_internal: string;
notes_client: string;
};
type Client = {
id: number;
company_name: string;
};
type WorkspaceUser = {
id: number;
name: string;
email: string;
};
type Props = {
form: Form<FolderFormData>;
folderTypeLabels: Record<string, string>;
folderStatusLabels: Record<string, string>;
folderPriorityLabels: Record<string, string>;
clients: Client[];
workspaceUsers: WorkspaceUser[];
submitLabel?: string;
};
const props = withDefaults(defineProps<Props>(), {
submitLabel: 'Enregistrer',
});
const emit = defineEmits<{
submit: [];
}>();
const currentYear = new Date().getFullYear();
const years = Array.from({ length: 10 }, (_, i) => currentYear - 2 + i);
const months = [
{ value: '', label: '—' },
...Array.from({ length: 12 }, (_, i) => ({
value: (i + 1).toString(),
label: `${i + 1}`,
})),
];
const quarters = [
{ value: '', label: '—' },
{ value: '1', label: 'T1 (JanMar)' },
{ value: '2', label: 'T2 (AvrJuin)' },
{ value: '3', label: 'T3 (JuilSep)' },
{ value: '4', label: 'T4 (OctDéc)' },
];
const isVatMonthly = () => props.form.type === 'vat_monthly';
const isVatQuarterly = () => props.form.type === 'vat_quarterly';
// Clear both period fields when type changes
watch(
() => props.form.type,
() => {
props.form.period_month = '';
props.form.period_quarter = '';
},
);
</script>
<template>
<form @submit.prevent="emit('submit')" class="flex flex-col space-y-6">
<div class="grid gap-2">
<Label for="client_id">Client</Label>
<select
id="client_id"
v-model="form.client_id"
required
class="border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
:aria-invalid="!!form.errors.client_id"
>
<option value="" disabled>Sélectionner un client</option>
<option
v-for="client in clients"
:key="client.id"
:value="client.id"
>
{{ client.company_name }}
</option>
</select>
<InputError :message="form.errors.client_id" />
</div>
<div class="grid gap-2">
<Label for="title">Titre</Label>
<Input
id="title"
v-model="form.title"
type="text"
required
placeholder="Ex. Déclaration TVA - T1 2026"
aria-invalid="!!form.errors.title"
/>
<InputError :message="form.errors.title" />
</div>
<div class="grid gap-2">
<Label for="type">Type</Label>
<select
id="type"
v-model="form.type"
required
class="border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
:aria-invalid="!!form.errors.type"
>
<option value="" disabled>Sélectionner un type</option>
<option
v-for="(label, value) in folderTypeLabels"
:key="value"
:value="value"
>
{{ label }}
</option>
</select>
<InputError :message="form.errors.type" />
</div>
<div class="grid gap-2 sm:grid-cols-3">
<div class="grid gap-2">
<Label for="period_year">Année</Label>
<select
id="period_year"
v-model="form.period_year"
required
class="border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
:aria-invalid="!!form.errors.period_year"
>
<option v-for="y in years" :key="y" :value="y">
{{ y }}
</option>
</select>
<InputError :message="form.errors.period_year" />
</div>
<template v-if="isVatMonthly()">
<div class="grid gap-2">
<Label for="period_month">Mois</Label>
<select
id="period_month"
v-model="form.period_month"
class="border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
:aria-invalid="!!form.errors.period_month"
>
<option
v-for="m in months"
:key="m.value"
:value="m.value"
>
{{ m.label }}
</option>
</select>
<InputError :message="form.errors.period_month" />
</div>
</template>
<template v-if="isVatQuarterly()">
<div class="grid gap-2">
<Label for="period_quarter">Trimestre</Label>
<select
id="period_quarter"
v-model="form.period_quarter"
class="border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
:aria-invalid="!!form.errors.period_quarter"
>
<option
v-for="q in quarters"
:key="q.value"
:value="q.value"
>
{{ q.label }}
</option>
</select>
<InputError :message="form.errors.period_quarter" />
</div>
</template>
</div>
<div class="grid gap-2 sm:grid-cols-2">
<div class="grid gap-2">
<Label for="due_date">Date limite</Label>
<Input
id="due_date"
v-model="form.due_date"
type="date"
aria-invalid="!!form.errors.due_date"
/>
<InputError :message="form.errors.due_date" />
</div>
<div class="grid gap-2">
<Label for="status">Statut</Label>
<select
id="status"
v-model="form.status"
class="border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
:aria-invalid="!!form.errors.status"
>
<option value="" disabled>Sélectionner un statut</option>
<option
v-for="(label, value) in folderStatusLabels"
:key="value"
:value="value"
>
{{ label }}
</option>
</select>
<InputError :message="form.errors.status" />
</div>
</div>
<div class="grid gap-2 sm:grid-cols-2">
<div class="grid gap-2">
<Label for="priority">Priorité</Label>
<select
id="priority"
v-model="form.priority"
class="border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
:aria-invalid="!!form.errors.priority"
>
<option value=""></option>
<option
v-for="(label, value) in folderPriorityLabels"
:key="value"
:value="value"
>
{{ label }}
</option>
</select>
<InputError :message="form.errors.priority" />
</div>
<div class="grid gap-2">
<Label for="assigned_to">Assigné à</Label>
<select
id="assigned_to"
v-model="form.assigned_to"
class="border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
:aria-invalid="!!form.errors.assigned_to"
>
<option :value="''"></option>
<option
v-for="user in workspaceUsers"
:key="user.id"
:value="user.id"
>
{{ user.name }} ({{ user.email }})
</option>
</select>
<InputError :message="form.errors.assigned_to" />
</div>
</div>
<div class="grid gap-2">
<Label for="notes_internal">Notes internes</Label>
<textarea
id="notes_internal"
v-model="form.notes_internal"
rows="3"
class="border-input bg-background w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
placeholder="Notes confidentielles"
:aria-invalid="!!form.errors.notes_internal"
/>
<InputError :message="form.errors.notes_internal" />
</div>
<div class="grid gap-2">
<Label for="notes_client">Notes client</Label>
<textarea
id="notes_client"
v-model="form.notes_client"
rows="3"
class="border-input bg-background w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
placeholder="Notes partagées avec le client"
:aria-invalid="!!form.errors.notes_client"
/>
<InputError :message="form.errors.notes_client" />
</div>
<div class="flex items-center gap-4">
<Button
type="submit"
:disabled="form.processing"
data-test="folder-form-submit"
>
<Spinner v-if="form.processing" />
{{ submitLabel }}
</Button>
</div>
</form>
</template>