Files
L-Ami-Fiduciaire/resources/js/components/ClientForm.vue

437 lines
17 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { Building2, Plus, Settings, Trash2, User } from 'lucide-vue-next';
import { computed } from 'vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Spinner } from '@/components/ui/spinner';
export type ClientContactData = {
id?: number;
full_name: string;
job_title: string;
email: string;
phone: string;
is_principal: boolean;
};
export type ClientFormData = {
company_name: string;
legal_form: string;
ice: string;
fiscal_id: string;
rc: string;
cnss: string;
patente: string;
contacts: ClientContactData[];
internal_responsible_id: number | string | '';
status: string;
internal_notes: string;
};
type WorkspaceUser = {
id: number;
name: string;
email: string;
};
type Props = {
form: ClientFormData & {
processing: boolean;
errors: Record<string, string>;
};
legalForms: Record<string, string>;
clientStatusLabels?: Record<string, string>;
workspaceUsers?: WorkspaceUser[];
submitLabel?: string;
};
const props = withDefaults(defineProps<Props>(), {
submitLabel: 'Enregistrer',
clientStatusLabels: () => ({}),
workspaceUsers: () => [],
});
const emit = defineEmits<{
submit: [];
}>();
const internalResponsibleSelect = computed({
get: () =>
props.form.internal_responsible_id === '' ||
props.form.internal_responsible_id == null
? '__none__'
: String(props.form.internal_responsible_id),
set: (v: string) => {
props.form.internal_responsible_id = v === '__none__' ? '' : v;
},
});
function addContact() {
props.form.contacts.push({
full_name: '',
job_title: '',
email: '',
phone: '',
is_principal: props.form.contacts.length === 0,
});
}
function removeContact(index: number) {
const wasPrincipal = props.form.contacts[index].is_principal;
props.form.contacts.splice(index, 1);
if (wasPrincipal && props.form.contacts.length > 0) {
props.form.contacts[0].is_principal = true;
}
}
function setPrincipal(index: number) {
props.form.contacts.forEach((c, i) => {
c.is_principal = i === index;
});
}
function handleSubmit() {
if (props.form.internal_responsible_id === '__none__') {
props.form.internal_responsible_id = '';
}
emit('submit');
}
const inputClass =
'h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50';
</script>
<template>
<form @submit.prevent="handleSubmit" class="space-y-6">
<!-- Société -->
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<Building2 class="size-4" />
Informations société
</CardTitle>
<CardDescription>
Identité légale et données fiscales de l'entreprise
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2 sm:col-span-2">
<Label for="company_name">Raison sociale</Label>
<Input
id="company_name"
v-model="form.company_name"
type="text"
required
placeholder="Nom officiel de l'entreprise"
:class="inputClass"
:aria-invalid="!!form.errors.company_name"
/>
<InputError :message="form.errors.company_name" />
</div>
<div class="space-y-2">
<Label for="legal_form">Forme juridique</Label>
<Select
v-model="form.legal_form"
required
:aria-invalid="!!form.errors.legal_form"
>
<SelectTrigger :class="['w-full', inputClass]">
<SelectValue placeholder="Sélectionner..." />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="(label, value) in legalForms"
:key="value"
:value="value"
>
{{ label }}
</SelectItem>
</SelectContent>
</Select>
<InputError :message="form.errors.legal_form" />
</div>
<div class="space-y-2">
<Label for="ice">ICE</Label>
<Input
id="ice"
v-model="form.ice"
type="text"
placeholder="Identifiant Commun de l'Entreprise"
:class="inputClass"
:aria-invalid="!!form.errors.ice"
/>
<InputError :message="form.errors.ice" />
</div>
<div class="space-y-2">
<Label for="fiscal_id">IF (Identifiant Fiscal)</Label>
<Input
id="fiscal_id"
v-model="form.fiscal_id"
type="text"
placeholder="Identifiant Fiscal"
:class="inputClass"
:aria-invalid="!!form.errors.fiscal_id"
/>
<InputError :message="form.errors.fiscal_id" />
</div>
<div class="space-y-2">
<Label for="rc">RC (Registre de Commerce)</Label>
<Input
id="rc"
v-model="form.rc"
type="text"
placeholder="Numéro RC"
:class="inputClass"
:aria-invalid="!!form.errors.rc"
/>
<InputError :message="form.errors.rc" />
</div>
<div class="space-y-2">
<Label for="cnss">CNSS</Label>
<Input
id="cnss"
v-model="form.cnss"
type="text"
placeholder="Numéro d'affiliation"
:class="inputClass"
:aria-invalid="!!form.errors.cnss"
/>
<InputError :message="form.errors.cnss" />
</div>
<div class="space-y-2">
<Label for="patente">Patente</Label>
<Input
id="patente"
v-model="form.patente"
type="text"
placeholder="Taxe professionnelle"
:class="inputClass"
:aria-invalid="!!form.errors.patente"
/>
<InputError :message="form.errors.patente" />
</div>
</div>
</CardContent>
</Card>
<!-- Responsables -->
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<User class="size-4" />
Responsables
</CardTitle>
<CardDescription>
Personnes à contacter pour les échanges et déclarations
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<InputError :message="form.errors.contacts" />
<div
v-for="(contact, index) in form.contacts"
:key="index"
class="space-y-4 rounded-lg border border-sidebar-border/70 p-4"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<label class="flex items-center gap-2 text-sm">
<input
type="radio"
:checked="contact.is_principal"
name="principal_contact"
class="accent-primary"
@change="setPrincipal(index)"
/>
Principal
</label>
</div>
<Button
v-if="form.contacts.length > 1"
type="button"
variant="ghost"
size="sm"
class="text-destructive"
@click="removeContact(index)"
>
<Trash2 class="size-4" />
</Button>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2 sm:col-span-2">
<Label :for="`contact_full_name_${index}`"
>Nom complet</Label
>
<Input
:id="`contact_full_name_${index}`"
v-model="contact.full_name"
type="text"
required
placeholder="Prénom Nom"
:class="inputClass"
:aria-invalid="
!!form.errors[`contacts.${index}.full_name`]
"
/>
<InputError
:message="
form.errors[`contacts.${index}.full_name`]
"
/>
</div>
<div class="space-y-2">
<Label :for="`contact_job_title_${index}`"
>Fonction</Label
>
<Input
:id="`contact_job_title_${index}`"
v-model="contact.job_title"
type="text"
placeholder="Ex. Directeur financier"
:class="inputClass"
/>
</div>
<div class="space-y-2">
<Label :for="`contact_email_${index}`">Email</Label>
<Input
:id="`contact_email_${index}`"
v-model="contact.email"
type="email"
placeholder="email@exemple.com"
:class="inputClass"
/>
</div>
<div class="space-y-2 sm:col-span-2">
<Label :for="`contact_phone_${index}`"
>Téléphone</Label
>
<Input
:id="`contact_phone_${index}`"
v-model="contact.phone"
type="tel"
placeholder="+212 6 00 00 00 00"
:class="inputClass"
/>
</div>
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
@click="addContact"
>
<Plus class="mr-1 size-4" />
Ajouter un responsable
</Button>
</CardContent>
</Card>
<!-- Suivi interne -->
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<Settings class="size-4" />
Suivi interne
</CardTitle>
<CardDescription>
Attribution et notes réservées au cabinet
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2">
<Label for="internal_responsible_id"
>Responsable interne</Label
>
<Select v-model="internalResponsibleSelect">
<SelectTrigger :class="['w-full', inputClass]">
<SelectValue
placeholder="Sélectionner un responsable"
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"
> Aucun </SelectItem
>
<SelectItem
v-for="user in workspaceUsers"
:key="user.id"
:value="String(user.id)"
>
{{ user.name }}
<span class="text-muted-foreground">
· {{ user.email }}</span
>
</SelectItem>
</SelectContent>
</Select>
<InputError
:message="form.errors.internal_responsible_id"
/>
</div>
<div class="space-y-2">
<Label for="status">Statut</Label>
<Select v-model="form.status">
<SelectTrigger :class="['w-full', inputClass]">
<SelectValue
placeholder="Sélectionner un statut"
/>
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="(label, value) in clientStatusLabels"
:key="value"
:value="value"
>
{{ label }}
</SelectItem>
</SelectContent>
</Select>
<InputError :message="form.errors.status" />
</div>
</div>
<div class="space-y-2">
<Label for="internal_notes">Notes internes</Label>
<textarea
id="internal_notes"
v-model="form.internal_notes"
rows="3"
:class="[inputClass, 'min-h-[80px] resize-y']"
placeholder="Notes confidentielles sur le client..."
:aria-invalid="!!form.errors.internal_notes"
/>
<InputError :message="form.errors.internal_notes" />
</div>
</CardContent>
</Card>
<div class="flex items-center gap-4">
<Button
type="submit"
:disabled="form.processing"
data-test="client-form-submit"
>
<Spinner v-if="form.processing" class="mr-2 size-4" />
{{ submitLabel }}
</Button>
</div>
</form>
</template>