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,35 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import AppearanceTabs from '@/components/AppearanceTabs.vue';
import Heading from '@/components/Heading.vue';
import AppLayout from '@/layouts/AppLayout.vue';
import SettingsLayout from '@/layouts/settings/Layout.vue';
import type { BreadcrumbItem } from '@/types';
import { edit } from '@/routes/appearance';
const breadcrumbItems: BreadcrumbItem[] = [
{
title: 'Appearance settings',
href: edit().url,
},
];
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbItems">
<Head title="Appearance settings" />
<h1 class="sr-only">Appearance Settings</h1>
<SettingsLayout>
<div class="space-y-6">
<Heading
variant="small"
title="Appearance settings"
description="Update your account's appearance settings"
/>
<AppearanceTabs />
</div>
</SettingsLayout>
</AppLayout>
</template>

View File

@@ -0,0 +1,116 @@
<script setup lang="ts">
import { Form, Head } from '@inertiajs/vue3';
import Heading from '@/components/Heading.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 AppLayout from '@/layouts/AppLayout.vue';
import SettingsLayout from '@/layouts/settings/Layout.vue';
import type { BreadcrumbItem } from '@/types';
import PasswordController from '@/actions/App/Http/Controllers/Settings/PasswordController';
import { edit } from '@/routes/user-password';
const breadcrumbItems: BreadcrumbItem[] = [
{
title: 'Password settings',
href: edit().url,
},
];
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbItems">
<Head title="Password settings" />
<h1 class="sr-only">Password Settings</h1>
<SettingsLayout>
<div class="space-y-6">
<Heading
variant="small"
title="Update password"
description="Ensure your account is using a long, random password to stay secure"
/>
<Form
v-bind="PasswordController.update.form()"
:options="{
preserveScroll: true,
}"
reset-on-success
:reset-on-error="[
'password',
'password_confirmation',
'current_password',
]"
class="space-y-6"
v-slot="{ errors, processing, recentlySuccessful }"
>
<div class="grid gap-2">
<Label for="current_password">Current password</Label>
<Input
id="current_password"
name="current_password"
type="password"
class="mt-1 block w-full"
autocomplete="current-password"
placeholder="Current password"
/>
<InputError :message="errors.current_password" />
</div>
<div class="grid gap-2">
<Label for="password">New password</Label>
<Input
id="password"
name="password"
type="password"
class="mt-1 block w-full"
autocomplete="new-password"
placeholder="New password"
/>
<InputError :message="errors.password" />
</div>
<div class="grid gap-2">
<Label for="password_confirmation"
>Confirm password</Label
>
<Input
id="password_confirmation"
name="password_confirmation"
type="password"
class="mt-1 block w-full"
autocomplete="new-password"
placeholder="Confirm password"
/>
<InputError :message="errors.password_confirmation" />
</div>
<div class="flex items-center gap-4">
<Button
:disabled="processing"
data-test="update-password-button"
>Save password</Button
>
<Transition
enter-active-class="transition ease-in-out"
enter-from-class="opacity-0"
leave-active-class="transition ease-in-out"
leave-to-class="opacity-0"
>
<p
v-show="recentlySuccessful"
class="text-sm text-neutral-600"
>
Saved.
</p>
</Transition>
</div>
</Form>
</div>
</SettingsLayout>
</AppLayout>
</template>

View File

@@ -0,0 +1,131 @@
<script setup lang="ts">
import { Form, Head, Link, usePage } from '@inertiajs/vue3';
import { computed } from 'vue';
import DeleteUser from '@/components/DeleteUser.vue';
import Heading from '@/components/Heading.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 AppLayout from '@/layouts/AppLayout.vue';
import SettingsLayout from '@/layouts/settings/Layout.vue';
import type { BreadcrumbItem } from '@/types';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import { edit } from '@/routes/profile';
import { send } from '@/routes/verification';
type Props = {
mustVerifyEmail: boolean;
status?: string;
};
defineProps<Props>();
const breadcrumbItems: BreadcrumbItem[] = [
{
title: 'Profile settings',
href: edit().url,
},
];
const page = usePage();
const user = computed(() => page.props.auth.user);
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbItems">
<Head title="Profile settings" />
<h1 class="sr-only">Profile Settings</h1>
<SettingsLayout>
<div class="flex flex-col space-y-6">
<Heading
variant="small"
title="Profile information"
description="Update your name and email address"
/>
<Form
v-bind="ProfileController.update.form()"
class="space-y-6"
v-slot="{ errors, processing, recentlySuccessful }"
>
<div class="grid gap-2">
<Label for="name">Name</Label>
<Input
id="name"
class="mt-1 block w-full"
name="name"
:default-value="user.name"
required
autocomplete="name"
placeholder="Full name"
/>
<InputError class="mt-2" :message="errors.name" />
</div>
<div class="grid gap-2">
<Label for="email">Email address</Label>
<Input
id="email"
type="email"
class="mt-1 block w-full"
name="email"
:default-value="user.email"
required
autocomplete="username"
placeholder="Email address"
/>
<InputError class="mt-2" :message="errors.email" />
</div>
<div v-if="mustVerifyEmail && !user.email_verified_at">
<p class="-mt-4 text-sm text-muted-foreground">
Your email address is unverified.
<Link
:href="send()"
as="button"
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
>
Click here to resend the verification email.
</Link>
</p>
<div
v-if="status === 'verification-link-sent'"
class="mt-2 text-sm font-medium text-green-600"
>
A new verification link has been sent to your email
address.
</div>
</div>
<div class="flex items-center gap-4">
<Button
:disabled="processing"
data-test="update-profile-button"
>Save</Button
>
<Transition
enter-active-class="transition ease-in-out"
enter-from-class="opacity-0"
leave-active-class="transition ease-in-out"
leave-to-class="opacity-0"
>
<p
v-show="recentlySuccessful"
class="text-sm text-neutral-600"
>
Saved.
</p>
</Transition>
</div>
</Form>
</div>
<DeleteUser />
</SettingsLayout>
</AppLayout>
</template>

View File

@@ -0,0 +1,125 @@
<script setup lang="ts">
import { Form, Head } from '@inertiajs/vue3';
import { ShieldBan, ShieldCheck } from 'lucide-vue-next';
import { onUnmounted, ref } from 'vue';
import Heading from '@/components/Heading.vue';
import TwoFactorRecoveryCodes from '@/components/TwoFactorRecoveryCodes.vue';
import TwoFactorSetupModal from '@/components/TwoFactorSetupModal.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useTwoFactorAuth } from '@/composables/useTwoFactorAuth';
import AppLayout from '@/layouts/AppLayout.vue';
import SettingsLayout from '@/layouts/settings/Layout.vue';
import type { BreadcrumbItem } from '@/types';
import { disable, enable, show } from '@/routes/two-factor';
type Props = {
requiresConfirmation?: boolean;
twoFactorEnabled?: boolean;
};
withDefaults(defineProps<Props>(), {
requiresConfirmation: false,
twoFactorEnabled: false,
});
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Two-Factor Authentication',
href: show.url(),
},
];
const { hasSetupData, clearTwoFactorAuthData } = useTwoFactorAuth();
const showSetupModal = ref<boolean>(false);
onUnmounted(() => {
clearTwoFactorAuthData();
});
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<Head title="Two-Factor Authentication" />
<h1 class="sr-only">Two-Factor Authentication Settings</h1>
<SettingsLayout>
<div class="space-y-6">
<Heading
variant="small"
title="Two-Factor Authentication"
description="Manage your two-factor authentication settings"
/>
<div
v-if="!twoFactorEnabled"
class="flex flex-col items-start justify-start space-y-4"
>
<Badge variant="destructive">Disabled</Badge>
<p class="text-muted-foreground">
When you enable two-factor authentication, you will be
prompted for a secure pin during login. This pin can be
retrieved from a TOTP-supported application on your
phone.
</p>
<div>
<Button
v-if="hasSetupData"
@click="showSetupModal = true"
>
<ShieldCheck />Continue Setup
</Button>
<Form
v-else
v-bind="enable.form()"
@success="showSetupModal = true"
#default="{ processing }"
>
<Button type="submit" :disabled="processing">
<ShieldCheck />Enable 2FA</Button
></Form
>
</div>
</div>
<div
v-else
class="flex flex-col items-start justify-start space-y-4"
>
<Badge variant="default">Enabled</Badge>
<p class="text-muted-foreground">
With two-factor authentication enabled, you will be
prompted for a secure, random pin during login, which
you can retrieve from the TOTP-supported application on
your phone.
</p>
<TwoFactorRecoveryCodes />
<div class="relative inline">
<Form v-bind="disable.form()" #default="{ processing }">
<Button
variant="destructive"
type="submit"
:disabled="processing"
>
<ShieldBan />
Disable 2FA
</Button>
</Form>
</div>
</div>
<TwoFactorSetupModal
v-model:isOpen="showSetupModal"
:requiresConfirmation="requiresConfirmation"
:twoFactorEnabled="twoFactorEnabled"
/>
</div>
</SettingsLayout>
</AppLayout>
</template>