feat: complete Epic 0 — foundation migration & infrastructure setup
Stories 0.2-0.5: rename folders→declarations (backend+frontend), configure Redis for cache/queue/sessions, add foundation database migrations (permissions, archived_at), replace DeclarationStatus enum with architecture lifecycle values, create DeclarationObserver for status transition validation and auto-archive, fix controller status transitions to respect observer rules. 93 tests pass (240 assertions). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { BookOpen, Briefcase, Building2, Folder, HelpCircle, LayoutGrid, Users } from 'lucide-vue-next';
|
||||
import {
|
||||
BookOpen,
|
||||
Briefcase,
|
||||
Building2,
|
||||
FileStack,
|
||||
HelpCircle,
|
||||
LayoutGrid,
|
||||
Users,
|
||||
} from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import NavFooter from '@/components/NavFooter.vue';
|
||||
import NavMain from '@/components/NavMain.vue';
|
||||
@@ -14,9 +22,9 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { dashboard } from '@/routes';
|
||||
import type { NavItem } from '@/types';
|
||||
import AppLogo from './AppLogo.vue';
|
||||
import { dashboard } from '@/routes';
|
||||
import WorkspaceSwitcher from './WorkspaceSwitcher.vue';
|
||||
|
||||
const page = usePage();
|
||||
@@ -36,9 +44,9 @@ const mainNavItems = computed<NavItem[]>(() => {
|
||||
icon: Briefcase,
|
||||
},
|
||||
{
|
||||
title: 'Dossiers',
|
||||
href: '/folders',
|
||||
icon: Folder,
|
||||
title: 'Déclarations',
|
||||
href: '/declarations',
|
||||
icon: FileStack,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -90,9 +98,16 @@ const footerNavItems: NavItem[] = [
|
||||
<SidebarContent>
|
||||
<NavMain :items="mainNavItems" />
|
||||
<template
|
||||
v-if="['admin', 'superadmin'].includes(String($page.props.auth.user?.group ?? ''))"
|
||||
v-if="
|
||||
['admin', 'superadmin'].includes(
|
||||
String($page.props.auth.user?.group ?? ''),
|
||||
)
|
||||
"
|
||||
>
|
||||
<NavMain :items="administrationNavItems" label="Administration" />
|
||||
<NavMain
|
||||
:items="administrationNavItems"
|
||||
label="Administration"
|
||||
/>
|
||||
</template>
|
||||
</SidebarContent>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<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';
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { Building2, Plus, Settings, Trash2, User } from 'lucide-vue-next';
|
||||
|
||||
export type ClientContactData = {
|
||||
id?: number;
|
||||
@@ -180,9 +180,7 @@ const inputClass =
|
||||
<InputError :message="form.errors.ice" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="fiscal_id"
|
||||
>IF (Identifiant Fiscal)</Label
|
||||
>
|
||||
<Label for="fiscal_id">IF (Identifiant Fiscal)</Label>
|
||||
<Input
|
||||
id="fiscal_id"
|
||||
v-model="form.fiscal_id"
|
||||
@@ -241,7 +239,7 @@ const inputClass =
|
||||
Responsables
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Personnes à contacter pour les échanges et dossiers
|
||||
Personnes à contacter pour les échanges et déclarations
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
@@ -288,16 +286,12 @@ const inputClass =
|
||||
placeholder="Prénom Nom"
|
||||
:class="inputClass"
|
||||
:aria-invalid="
|
||||
!!form.errors[
|
||||
`contacts.${index}.full_name`
|
||||
]
|
||||
!!form.errors[`contacts.${index}.full_name`]
|
||||
"
|
||||
/>
|
||||
<InputError
|
||||
:message="
|
||||
form.errors[
|
||||
`contacts.${index}.full_name`
|
||||
]
|
||||
form.errors[`contacts.${index}.full_name`]
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@@ -314,9 +308,7 @@ const inputClass =
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label :for="`contact_email_${index}`"
|
||||
>Email</Label
|
||||
>
|
||||
<Label :for="`contact_email_${index}`">Email</Label>
|
||||
<Input
|
||||
:id="`contact_email_${index}`"
|
||||
v-model="contact.email"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
|
||||
export type FolderFormData = {
|
||||
export type DeclarationFormData = {
|
||||
client_id: number | '';
|
||||
title: string;
|
||||
type: string;
|
||||
@@ -34,10 +34,10 @@ type WorkspaceUser = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
form: Form<FolderFormData>;
|
||||
folderTypeLabels: Record<string, string>;
|
||||
folderStatusLabels: Record<string, string>;
|
||||
folderPriorityLabels: Record<string, string>;
|
||||
form: Form<DeclarationFormData>;
|
||||
declarationTypeLabels: Record<string, string>;
|
||||
declarationStatusLabels: Record<string, string>;
|
||||
declarationPriorityLabels: Record<string, string>;
|
||||
clients: Client[];
|
||||
workspaceUsers: WorkspaceUser[];
|
||||
submitLabel?: string;
|
||||
@@ -89,7 +89,7 @@ watch(
|
||||
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"
|
||||
class="h-10 w-full rounded-md border border-input bg-background 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>
|
||||
@@ -123,12 +123,12 @@ watch(
|
||||
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"
|
||||
class="h-10 w-full rounded-md border border-input bg-background 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"
|
||||
v-for="(label, value) in declarationTypeLabels"
|
||||
:key="value"
|
||||
:value="value"
|
||||
>
|
||||
@@ -145,7 +145,7 @@ watch(
|
||||
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"
|
||||
class="h-10 w-full rounded-md border border-input bg-background 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">
|
||||
@@ -160,7 +160,7 @@ watch(
|
||||
<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"
|
||||
class="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
|
||||
:aria-invalid="!!form.errors.period_month"
|
||||
>
|
||||
<option
|
||||
@@ -180,7 +180,7 @@ watch(
|
||||
<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"
|
||||
class="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
|
||||
:aria-invalid="!!form.errors.period_quarter"
|
||||
>
|
||||
<option
|
||||
@@ -212,12 +212,12 @@ watch(
|
||||
<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"
|
||||
class="h-10 w-full rounded-md border border-input bg-background 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"
|
||||
v-for="(label, value) in declarationStatusLabels"
|
||||
:key="value"
|
||||
:value="value"
|
||||
>
|
||||
@@ -234,12 +234,12 @@ watch(
|
||||
<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"
|
||||
class="h-10 w-full rounded-md border border-input bg-background 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"
|
||||
v-for="(label, value) in declarationPriorityLabels"
|
||||
:key="value"
|
||||
:value="value"
|
||||
>
|
||||
@@ -253,7 +253,7 @@ watch(
|
||||
<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"
|
||||
class="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
|
||||
:aria-invalid="!!form.errors.assigned_to"
|
||||
>
|
||||
<option :value="''">—</option>
|
||||
@@ -275,7 +275,7 @@ watch(
|
||||
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"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring"
|
||||
placeholder="Notes confidentielles"
|
||||
:aria-invalid="!!form.errors.notes_internal"
|
||||
/>
|
||||
@@ -288,7 +288,7 @@ watch(
|
||||
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"
|
||||
class="w-full rounded-md border border-input bg-background 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"
|
||||
/>
|
||||
@@ -299,7 +299,7 @@ watch(
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
data-test="folder-form-submit"
|
||||
data-test="declaration-form-submit"
|
||||
>
|
||||
<Spinner v-if="form.processing" />
|
||||
{{ submitLabel }}
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { router, usePage } from '@inertiajs/vue3';
|
||||
import { Bell } from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -16,8 +16,8 @@ type NotificationItem = {
|
||||
id: string;
|
||||
type: string;
|
||||
data: {
|
||||
folder_id?: number;
|
||||
folder_title?: string;
|
||||
declaration_id?: number;
|
||||
declaration_title?: string;
|
||||
mentioned_by_name?: string;
|
||||
message?: string;
|
||||
url?: string;
|
||||
@@ -36,7 +36,8 @@ type UserNotifications = {
|
||||
const page = usePage();
|
||||
|
||||
const userNotifications = computed<UserNotifications>(() => {
|
||||
return (page.props as Record<string, unknown>).userNotifications as UserNotifications;
|
||||
return (page.props as Record<string, unknown>)
|
||||
.userNotifications as UserNotifications;
|
||||
});
|
||||
|
||||
const unreadCount = computed(() => userNotifications.value?.unread_count ?? 0);
|
||||
@@ -63,7 +64,7 @@ function navigateToNotification(notification: NotificationItem) {
|
||||
|
||||
router.visit(targetUrl, {
|
||||
onError: () => {
|
||||
// Folder may have been deleted — mark as read anyway
|
||||
// Declaration may have been deleted — mark as read anyway
|
||||
if (!notification.read_at) {
|
||||
markAsRead(notification);
|
||||
}
|
||||
@@ -86,7 +87,7 @@ function markAllAsRead() {
|
||||
<Bell class="size-4" />
|
||||
<span
|
||||
v-if="unreadCount > 0"
|
||||
class="absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-destructive text-[10px] font-bold text-destructive-foreground"
|
||||
class="absolute -top-0.5 -right-0.5 flex size-4 items-center justify-center rounded-full bg-destructive text-[10px] font-bold text-destructive-foreground"
|
||||
>
|
||||
{{ unreadCount > 9 ? '9+' : unreadCount }}
|
||||
</span>
|
||||
@@ -96,11 +97,17 @@ function markAllAsRead() {
|
||||
<DropdownMenuLabel>Notifications</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<div v-if="isLoading" class="px-2 py-4 text-center text-sm text-muted-foreground">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="px-2 py-4 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
Chargement...
|
||||
</div>
|
||||
|
||||
<div v-else-if="!items.length" class="px-2 py-4 text-center text-sm text-muted-foreground">
|
||||
<div
|
||||
v-else-if="!items.length"
|
||||
class="px-2 py-4 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
Aucune notification.
|
||||
</div>
|
||||
|
||||
@@ -114,17 +121,27 @@ function markAllAsRead() {
|
||||
>
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<span class="text-xs font-medium">
|
||||
{{ notification.data?.mentioned_by_name ?? 'Système' }}
|
||||
{{
|
||||
notification.data?.mentioned_by_name ??
|
||||
'Système'
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ notification.created_at }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
<span v-if="notification.data?.folder_title" class="font-medium text-foreground">
|
||||
{{ notification.data.folder_title }}
|
||||
<span
|
||||
v-if="notification.data?.declaration_title"
|
||||
class="font-medium text-foreground"
|
||||
>
|
||||
{{ notification.data.declaration_title }}
|
||||
</span>
|
||||
{{ notification.data?.message ? ` — ${notification.data.message}` : '' }}
|
||||
{{
|
||||
notification.data?.message
|
||||
? ` — ${notification.data.message}`
|
||||
: ''
|
||||
}}
|
||||
</p>
|
||||
<span
|
||||
v-if="!notification.read_at"
|
||||
|
||||
@@ -1,42 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-vue-next';
|
||||
import { computed, ref } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-vue-next';
|
||||
|
||||
type Folder = {
|
||||
type Declaration = {
|
||||
id: number;
|
||||
due_date: string | null;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
folders: Folder[];
|
||||
declarations: Declaration[];
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const monthNames = [
|
||||
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre',
|
||||
'Janvier',
|
||||
'Février',
|
||||
'Mars',
|
||||
'Avril',
|
||||
'Mai',
|
||||
'Juin',
|
||||
'Juillet',
|
||||
'Août',
|
||||
'Septembre',
|
||||
'Octobre',
|
||||
'Novembre',
|
||||
'Décembre',
|
||||
];
|
||||
const dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
|
||||
|
||||
const current = ref(new Date());
|
||||
|
||||
const monthLabel = computed(() =>
|
||||
`${monthNames[current.value.getMonth()]} ${current.value.getFullYear()}`,
|
||||
const monthLabel = computed(
|
||||
() =>
|
||||
`${monthNames[current.value.getMonth()]} ${current.value.getFullYear()}`,
|
||||
);
|
||||
|
||||
const datesWithFolders = computed(() => {
|
||||
const datesWithDeclarations = computed(() => {
|
||||
const set = new Set<string>();
|
||||
props.folders.forEach((f) => {
|
||||
props.declarations.forEach((f) => {
|
||||
if (f.due_date) set.add(f.due_date);
|
||||
});
|
||||
return set;
|
||||
});
|
||||
|
||||
const foldersByDate = computed(() => {
|
||||
const declarationsByDate = computed(() => {
|
||||
const map = new Map<string, number>();
|
||||
props.folders.forEach((f) => {
|
||||
props.declarations.forEach((f) => {
|
||||
if (f.due_date) {
|
||||
map.set(f.due_date, (map.get(f.due_date) ?? 0) + 1);
|
||||
}
|
||||
@@ -52,7 +63,11 @@ const calendarDays = computed(() => {
|
||||
const startDay = (first.getDay() + 6) % 7;
|
||||
const daysInMonth = last.getDate();
|
||||
|
||||
const days: Array<{ date: Date | null; dateStr: string | null; count: number }> = [];
|
||||
const days: Array<{
|
||||
date: Date | null;
|
||||
dateStr: string | null;
|
||||
count: number;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < startDay; i++) {
|
||||
days.push({ date: null, dateStr: null, count: 0 });
|
||||
@@ -63,18 +78,24 @@ const calendarDays = computed(() => {
|
||||
days.push({
|
||||
date,
|
||||
dateStr,
|
||||
count: foldersByDate.value.get(dateStr) ?? 0,
|
||||
count: declarationsByDate.value.get(dateStr) ?? 0,
|
||||
});
|
||||
}
|
||||
return days;
|
||||
});
|
||||
|
||||
function prevMonth() {
|
||||
current.value = new Date(current.value.getFullYear(), current.value.getMonth() - 1);
|
||||
current.value = new Date(
|
||||
current.value.getFullYear(),
|
||||
current.value.getMonth() - 1,
|
||||
);
|
||||
}
|
||||
|
||||
function nextMonth() {
|
||||
current.value = new Date(current.value.getFullYear(), current.value.getMonth() + 1);
|
||||
current.value = new Date(
|
||||
current.value.getFullYear(),
|
||||
current.value.getMonth() + 1,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -111,11 +132,8 @@ function nextMonth() {
|
||||
>
|
||||
<template v-if="cell.date">
|
||||
{{ cell.date.getDate() }}
|
||||
<span
|
||||
v-if="cell.count > 0"
|
||||
class="mt-0.5 text-[10px]"
|
||||
>
|
||||
{{ cell.count }} dossier{{ cell.count > 1 ? 's' : '' }}
|
||||
<span v-if="cell.count > 0" class="mt-0.5 text-[10px]">
|
||||
{{ cell.count }} décl.
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CheckCircle2, Download, FileText } from 'lucide-vue-next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
type Props = {
|
||||
message: {
|
||||
@@ -27,13 +27,19 @@ const props = defineProps<Props>();
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
invite: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
situation: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
file_request: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
confirmation: 'bg-violet-100 text-violet-800 dark:bg-violet-900/30 dark:text-violet-300',
|
||||
situation:
|
||||
'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
file_request:
|
||||
'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
confirmation:
|
||||
'bg-violet-100 text-violet-800 dark:bg-violet-900/30 dark:text-violet-300',
|
||||
text: 'bg-slate-100 text-slate-700 dark:bg-slate-800/50 dark:text-slate-300',
|
||||
};
|
||||
|
||||
const confirmationStatusLabels: Record<string, { label: string; class: string }> = {
|
||||
const confirmationStatusLabels: Record<
|
||||
string,
|
||||
{ label: string; class: string }
|
||||
> = {
|
||||
pending: {
|
||||
label: 'En attente',
|
||||
class: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
@@ -80,12 +86,18 @@ function getTypeColor(type: string): string {
|
||||
<span
|
||||
v-if="message.confirmation_status"
|
||||
class="inline-flex rounded px-2 py-0.5 text-xs font-medium"
|
||||
:class="confirmationStatusLabels[message.confirmation_status]?.class ?? ''"
|
||||
:class="
|
||||
confirmationStatusLabels[message.confirmation_status]
|
||||
?.class ?? ''
|
||||
"
|
||||
>
|
||||
{{ confirmationStatusLabels[message.confirmation_status]?.label ?? message.confirmation_status }}
|
||||
{{
|
||||
confirmationStatusLabels[message.confirmation_status]
|
||||
?.label ?? message.confirmation_status
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-2 whitespace-pre-wrap text-sm">{{ message.body }}</p>
|
||||
<p class="mt-2 text-sm whitespace-pre-wrap">{{ message.body }}</p>
|
||||
<div
|
||||
v-if="message.attachments?.length"
|
||||
class="mt-3 flex flex-wrap gap-2"
|
||||
@@ -95,31 +107,30 @@ function getTypeColor(type: string): string {
|
||||
:key="att.id"
|
||||
class="flex items-center gap-2 rounded-lg border border-sidebar-border/70 bg-background/50 p-2"
|
||||
>
|
||||
<div class="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded bg-muted">
|
||||
<div
|
||||
class="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded bg-muted"
|
||||
>
|
||||
<img
|
||||
v-if="isImageMime(att.mime_type)"
|
||||
:src="att.downloadUrl"
|
||||
:alt="att.file_name"
|
||||
class="size-10 object-cover"
|
||||
/>
|
||||
<FileText
|
||||
v-else
|
||||
class="size-5 text-muted-foreground"
|
||||
/>
|
||||
<FileText v-else class="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="inline-flex items-center gap-1.5 truncate text-xs font-medium">
|
||||
<p
|
||||
class="inline-flex items-center gap-1.5 truncate text-xs font-medium"
|
||||
>
|
||||
{{ att.file_name }}
|
||||
<CheckCircle2 v-if="att.is_downloaded" class="size-3.5 text-green-500" />
|
||||
<CheckCircle2
|
||||
v-if="att.is_downloaded"
|
||||
class="size-3.5 text-green-500"
|
||||
/>
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">{{ att.size }}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
as-child
|
||||
class="shrink-0"
|
||||
>
|
||||
<Button variant="ghost" size="sm" as-child class="shrink-0">
|
||||
<a
|
||||
:href="att.downloadUrl"
|
||||
target="_blank"
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import {
|
||||
Briefcase,
|
||||
@@ -11,17 +10,18 @@ import {
|
||||
FileCheck,
|
||||
MessageSquareWarning,
|
||||
ArrowRight,
|
||||
Folder,
|
||||
FileStack,
|
||||
} from 'lucide-vue-next';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { dashboard } from '@/routes';
|
||||
import { computed } from 'vue';
|
||||
import PlaceholderPattern from '@/components/PlaceholderPattern.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { dashboard } from '@/routes';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
|
||||
type AssignedFolder = {
|
||||
type AssignedDeclaration = {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
@@ -41,7 +41,7 @@ type NotificationItem = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
assignedFolders: AssignedFolder[];
|
||||
assignedDeclarations: AssignedDeclaration[];
|
||||
notifications: {
|
||||
overdue: NotificationItem[];
|
||||
due_soon: NotificationItem[];
|
||||
@@ -49,7 +49,7 @@ type Props = {
|
||||
awaiting_validation: NotificationItem[];
|
||||
};
|
||||
workspaceName: string | null;
|
||||
foldersUrl: string | null;
|
||||
declarationsUrl: string | null;
|
||||
clientsUrl: string | null;
|
||||
};
|
||||
|
||||
@@ -87,7 +87,10 @@ const statusLabels: Record<string, string> = {
|
||||
cancelled: 'Annulé',
|
||||
};
|
||||
|
||||
const statusVariant: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
||||
const statusVariant: Record<
|
||||
string,
|
||||
'default' | 'secondary' | 'destructive' | 'outline'
|
||||
> = {
|
||||
draft: 'secondary',
|
||||
waiting_documents: 'outline',
|
||||
documents_received: 'default',
|
||||
@@ -132,45 +135,75 @@ const hasAnyNotifications = computed(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Dashboard" />
|
||||
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<div class="flex h-full flex-1 flex-col gap-6 overflow-x-auto rounded-xl p-4">
|
||||
<div
|
||||
class="flex h-full flex-1 flex-col gap-6 overflow-x-auto rounded-xl p-4"
|
||||
>
|
||||
<!-- Quick links when no workspace -->
|
||||
<div v-if="!hasWorkspace" class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<Link href="/users"
|
||||
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border">
|
||||
<div
|
||||
v-if="!hasWorkspace"
|
||||
class="grid auto-rows-min gap-4 md:grid-cols-3"
|
||||
>
|
||||
<Link
|
||||
href="/users"
|
||||
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border"
|
||||
>
|
||||
<Users class="h-8 w-8" />
|
||||
<span class="font-medium">Users</span>
|
||||
<span class="text-xs text-muted-foreground">Manage users</span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>Manage users</span
|
||||
>
|
||||
</Link>
|
||||
<Link href="/workspaces"
|
||||
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border">
|
||||
<Link
|
||||
href="/workspaces"
|
||||
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border"
|
||||
>
|
||||
<Building2 class="h-8 w-8" />
|
||||
<span class="font-medium">Workspaces</span>
|
||||
<span class="text-xs text-muted-foreground">Cabinets comptables</span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>Cabinets comptables</span
|
||||
>
|
||||
</Link>
|
||||
<Link v-if="clientsUrl" :href="clientsUrl"
|
||||
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border">
|
||||
<Link
|
||||
v-if="clientsUrl"
|
||||
:href="clientsUrl"
|
||||
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border"
|
||||
>
|
||||
<Briefcase class="h-8 w-8" />
|
||||
<span class="font-medium">Clients</span>
|
||||
<span class="text-xs text-muted-foreground">Manage clients</span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>Manage clients</span
|
||||
>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div v-if="hasWorkspace" class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<Link href="/folders"
|
||||
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border">
|
||||
<Folder class="h-8 w-8" />
|
||||
<span class="font-medium">Dossiers</span>
|
||||
<span class="text-xs text-muted-foreground">Manage folders</span>
|
||||
<div
|
||||
v-if="hasWorkspace"
|
||||
class="grid auto-rows-min gap-4 md:grid-cols-3"
|
||||
>
|
||||
<Link
|
||||
v-if="declarationsUrl"
|
||||
:href="declarationsUrl"
|
||||
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border"
|
||||
>
|
||||
<FileStack class="h-8 w-8" />
|
||||
<span class="font-medium">Déclarations</span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>Gérer les déclarations</span
|
||||
>
|
||||
</Link>
|
||||
<Link v-if="clientsUrl" :href="clientsUrl"
|
||||
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border">
|
||||
<Link
|
||||
v-if="clientsUrl"
|
||||
:href="clientsUrl"
|
||||
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border"
|
||||
>
|
||||
<Briefcase class="h-8 w-8" />
|
||||
<span class="font-medium">Clients</span>
|
||||
<span class="text-xs text-muted-foreground">Manage clients</span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>Manage clients</span
|
||||
>
|
||||
</Link>
|
||||
<div
|
||||
class="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
|
||||
@@ -185,21 +218,34 @@ const hasAnyNotifications = computed(
|
||||
<div v-if="hasAnyNotifications" class="space-y-4">
|
||||
<h2 class="text-lg font-semibold">À traiter</h2>
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card v-if="notifications.overdue.length > 0" class="border-destructive/50 bg-destructive/5">
|
||||
<Card
|
||||
v-if="notifications.overdue.length > 0"
|
||||
class="border-destructive/50 bg-destructive/5"
|
||||
>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<AlertTriangle class="h-4 w-4 text-destructive" />
|
||||
<CardTitle
|
||||
class="flex items-center gap-2 text-base"
|
||||
>
|
||||
<AlertTriangle
|
||||
class="h-4 w-4 text-destructive"
|
||||
/>
|
||||
En retard
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<Link v-for="item in notifications.overdue" :key="item.id" :href="item.showUrl"
|
||||
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50">
|
||||
<Link
|
||||
v-for="item in notifications.overdue"
|
||||
:key="item.id"
|
||||
:href="item.showUrl"
|
||||
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div class="truncate">
|
||||
<span class="font-medium">{{
|
||||
item.title
|
||||
}}</span>
|
||||
<span class="ml-1 text-muted-foreground">
|
||||
}}</span>
|
||||
<span
|
||||
class="ml-1 text-muted-foreground"
|
||||
>
|
||||
{{ item.client_name }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -207,21 +253,32 @@ const hasAnyNotifications = computed(
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card v-if="notifications.due_soon.length > 0" class="border-amber-500/50 bg-amber-500/5">
|
||||
<Card
|
||||
v-if="notifications.due_soon.length > 0"
|
||||
class="border-amber-500/50 bg-amber-500/5"
|
||||
>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<CardTitle
|
||||
class="flex items-center gap-2 text-base"
|
||||
>
|
||||
<Clock class="h-4 w-4 text-amber-600" />
|
||||
Échéance proche
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<Link v-for="item in notifications.due_soon" :key="item.id" :href="item.showUrl"
|
||||
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50">
|
||||
<Link
|
||||
v-for="item in notifications.due_soon"
|
||||
:key="item.id"
|
||||
:href="item.showUrl"
|
||||
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div class="truncate">
|
||||
<span class="font-medium">{{
|
||||
item.title
|
||||
}}</span>
|
||||
<span class="ml-1 text-muted-foreground">
|
||||
}}</span>
|
||||
<span
|
||||
class="ml-1 text-muted-foreground"
|
||||
>
|
||||
{{ item.client_name }} —
|
||||
{{ item.due_date }}
|
||||
</span>
|
||||
@@ -230,22 +287,32 @@ const hasAnyNotifications = computed(
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card v-if="notifications.documents_received.length > 0" class="border-primary/50 bg-primary/5">
|
||||
<Card
|
||||
v-if="notifications.documents_received.length > 0"
|
||||
class="border-primary/50 bg-primary/5"
|
||||
>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<CardTitle
|
||||
class="flex items-center gap-2 text-base"
|
||||
>
|
||||
<FileCheck class="h-4 w-4 text-primary" />
|
||||
Documents reçus
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<Link v-for="item in notifications.documents_received" :key="item.id"
|
||||
<Link
|
||||
v-for="item in notifications.documents_received"
|
||||
:key="item.id"
|
||||
:href="item.showUrl"
|
||||
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50">
|
||||
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div class="truncate">
|
||||
<span class="font-medium">{{
|
||||
item.title
|
||||
}}</span>
|
||||
<span class="ml-1 text-muted-foreground">
|
||||
}}</span>
|
||||
<span
|
||||
class="ml-1 text-muted-foreground"
|
||||
>
|
||||
{{ item.client_name }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -253,23 +320,34 @@ const hasAnyNotifications = computed(
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card v-if="notifications.awaiting_validation.length > 0"
|
||||
class="border-blue-500/50 bg-blue-500/5">
|
||||
<Card
|
||||
v-if="notifications.awaiting_validation.length > 0"
|
||||
class="border-blue-500/50 bg-blue-500/5"
|
||||
>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<MessageSquareWarning class="h-4 w-4 text-blue-600" />
|
||||
<CardTitle
|
||||
class="flex items-center gap-2 text-base"
|
||||
>
|
||||
<MessageSquareWarning
|
||||
class="h-4 w-4 text-blue-600"
|
||||
/>
|
||||
En attente validation client
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<Link v-for="item in notifications.awaiting_validation" :key="item.id"
|
||||
<Link
|
||||
v-for="item in notifications.awaiting_validation"
|
||||
:key="item.id"
|
||||
:href="item.showUrl"
|
||||
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50">
|
||||
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div class="truncate">
|
||||
<span class="font-medium">{{
|
||||
item.title
|
||||
}}</span>
|
||||
<span class="ml-1 text-muted-foreground">
|
||||
}}</span>
|
||||
<span
|
||||
class="ml-1 text-muted-foreground"
|
||||
>
|
||||
{{ item.client_name }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -280,92 +358,151 @@ const hasAnyNotifications = computed(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- My assigned dossiers -->
|
||||
<!-- My assigned declarations -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">
|
||||
Mes dossiers — {{ workspaceName }}
|
||||
Mes déclarations — {{ workspaceName }}
|
||||
</h2>
|
||||
<Button v-if="foldersUrl" variant="outline" as-child>
|
||||
<Link :href="foldersUrl">
|
||||
Tous les dossiers
|
||||
<Button
|
||||
v-if="declarationsUrl"
|
||||
variant="outline"
|
||||
as-child
|
||||
>
|
||||
<Link :href="declarationsUrl">
|
||||
Toutes les déclarations
|
||||
<ArrowRight class="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card v-if="assignedFolders.length > 0" class="overflow-hidden">
|
||||
<Card
|
||||
v-if="assignedDeclarations.length > 0"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="border-b border-sidebar-border/70 bg-muted/50">
|
||||
<thead
|
||||
class="border-b border-sidebar-border/70 bg-muted/50"
|
||||
>
|
||||
<tr>
|
||||
<th class="h-10 px-4 text-left font-medium">
|
||||
Dossier / Client
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium"
|
||||
>
|
||||
Déclaration / Client
|
||||
</th>
|
||||
<th class="h-10 px-4 text-left font-medium">
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th class="h-10 px-4 text-left font-medium">
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium"
|
||||
>
|
||||
Statut
|
||||
</th>
|
||||
<th class="h-10 px-4 text-left font-medium">
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium"
|
||||
>
|
||||
Progression
|
||||
</th>
|
||||
<th class="h-10 px-4 text-left font-medium">
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium"
|
||||
>
|
||||
Date limite
|
||||
</th>
|
||||
<th class="h-10 w-10 px-4"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="folder in assignedFolders" :key="folder.id"
|
||||
class="border-b border-sidebar-border/50 last:border-0 transition-colors hover:bg-muted/30">
|
||||
<tr
|
||||
v-for="declaration in assignedDeclarations"
|
||||
:key="declaration.id"
|
||||
class="border-b border-sidebar-border/50 transition-colors last:border-0 hover:bg-muted/30"
|
||||
>
|
||||
<td class="px-4 py-3">
|
||||
<Link :href="folder.showUrl" class="block font-medium hover:underline">
|
||||
{{ folder.title }}
|
||||
<Link
|
||||
:href="declaration.showUrl"
|
||||
class="block font-medium hover:underline"
|
||||
>
|
||||
{{ declaration.title }}
|
||||
</Link>
|
||||
<span class="block text-xs text-muted-foreground">
|
||||
{{ folder.client_name }}
|
||||
<span
|
||||
class="block text-xs text-muted-foreground"
|
||||
>
|
||||
{{ declaration.client_name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-muted-foreground">
|
||||
{{ typeLabel(folder.type) }}
|
||||
<td
|
||||
class="px-4 py-3 text-muted-foreground"
|
||||
>
|
||||
{{ typeLabel(declaration.type) }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<Badge :variant="statusVariant[folder.status] ?? 'secondary'
|
||||
">
|
||||
<Badge
|
||||
:variant="
|
||||
statusVariant[
|
||||
declaration.status
|
||||
] ?? 'secondary'
|
||||
"
|
||||
>
|
||||
{{
|
||||
statusLabel(folder.status)
|
||||
statusLabel(
|
||||
declaration.status,
|
||||
)
|
||||
}}
|
||||
</Badge>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex h-2 w-24 overflow-hidden rounded-full bg-muted">
|
||||
<div class="h-full bg-primary transition-all" :style="{
|
||||
width: `${progressPercent(folder.status)}%`,
|
||||
}" />
|
||||
<div
|
||||
class="flex h-2 w-24 overflow-hidden rounded-full bg-muted"
|
||||
>
|
||||
<div
|
||||
class="h-full bg-primary transition-all"
|
||||
:style="{
|
||||
width: `${progressPercent(declaration.status)}%`,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ progressPercent(folder.status) }}%
|
||||
<span
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
{{
|
||||
progressPercent(
|
||||
declaration.status,
|
||||
)
|
||||
}}%
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span :class="{
|
||||
'text-destructive font-medium':
|
||||
folder.due_date &&
|
||||
folder.due_date <
|
||||
new Date()
|
||||
.toISOString()
|
||||
.slice(0, 10),
|
||||
}">
|
||||
{{ folder.due_date || '—' }}
|
||||
<span
|
||||
:class="{
|
||||
'font-medium text-destructive':
|
||||
declaration.due_date &&
|
||||
declaration.due_date <
|
||||
new Date()
|
||||
.toISOString()
|
||||
.slice(0, 10),
|
||||
}"
|
||||
>
|
||||
{{
|
||||
declaration.due_date || '—'
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<Button variant="ghost" size="sm" as-child>
|
||||
<Link :href="folder.showUrl">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
as-child
|
||||
>
|
||||
<Link
|
||||
:href="declaration.showUrl"
|
||||
>
|
||||
Voir
|
||||
<ArrowRight class="ml-1 h-3 w-3" />
|
||||
<ArrowRight
|
||||
class="ml-1 h-3 w-3"
|
||||
/>
|
||||
</Link>
|
||||
</Button>
|
||||
</td>
|
||||
@@ -376,13 +513,20 @@ const hasAnyNotifications = computed(
|
||||
</Card>
|
||||
|
||||
<Card v-else>
|
||||
<CardContent class="flex flex-col items-center justify-center py-12">
|
||||
<FolderOpen class="mb-3 h-12 w-12 text-muted-foreground" />
|
||||
<CardContent
|
||||
class="flex flex-col items-center justify-center py-12"
|
||||
>
|
||||
<FolderOpen
|
||||
class="mb-3 h-12 w-12 text-muted-foreground"
|
||||
/>
|
||||
<p class="mb-2 text-muted-foreground">
|
||||
Aucun dossier ne vous est assigné pour le moment.
|
||||
Aucune déclaration ne vous est assignée pour le
|
||||
moment.
|
||||
</p>
|
||||
<Button v-if="foldersUrl" as-child>
|
||||
<Link :href="foldersUrl">Voir tous les dossiers</Link>
|
||||
<Button v-if="declarationsUrl" as-child>
|
||||
<Link :href="declarationsUrl"
|
||||
>Voir toutes les déclarations</Link
|
||||
>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { dashboard, login, register } from '@/routes';
|
||||
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Building2,
|
||||
Calendar,
|
||||
@@ -13,6 +8,17 @@ import {
|
||||
Shield,
|
||||
Users,
|
||||
} from 'lucide-vue-next';
|
||||
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { dashboard, login, register } from '@/routes';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
@@ -25,12 +31,16 @@ withDefaults(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="`${$page.props.name} — Gestion des dossiers fiscaux`" />
|
||||
<Head :title="`${$page.props.name} — Gestion des déclarations fiscales`" />
|
||||
|
||||
<div class="min-h-screen bg-background">
|
||||
<!-- Header -->
|
||||
<header class="sticky top-0 z-50 border-b border-sidebar-border/70 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div class="mx-auto flex h-16 max-w-6xl items-center justify-between px-4">
|
||||
<header
|
||||
class="sticky top-0 z-50 border-b border-sidebar-border/70 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
||||
>
|
||||
<div
|
||||
class="mx-auto flex h-16 max-w-6xl items-center justify-between px-4"
|
||||
>
|
||||
<Link href="/" class="flex items-center gap-2 font-semibold">
|
||||
<AppLogoIcon class="size-8 fill-current text-primary" />
|
||||
<span>{{ $page.props.name }}</span>
|
||||
@@ -57,23 +67,34 @@ withDefaults(
|
||||
<!-- Hero -->
|
||||
<section class="px-4 py-20 md:py-28">
|
||||
<div class="mx-auto max-w-4xl text-center">
|
||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
|
||||
<h1
|
||||
class="text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl"
|
||||
>
|
||||
Simplifiez la gestion des
|
||||
<span class="text-primary">dossiers fiscaux</span>
|
||||
<span class="text-primary">déclarations fiscales</span>
|
||||
</h1>
|
||||
<p class="mt-6 max-w-2xl mx-auto text-lg text-muted-foreground">
|
||||
Plateforme dédiée aux cabinets d'expertise comptable au Maroc.
|
||||
Centralisez les documents, les demandes de pièces et les validations clients en un seul endroit.
|
||||
<p
|
||||
class="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground"
|
||||
>
|
||||
Plateforme dédiée aux cabinets d'expertise comptable au
|
||||
Maroc. Centralisez les documents, les demandes de pièces
|
||||
et les validations clients en un seul endroit.
|
||||
</p>
|
||||
<div class="mt-10 flex flex-wrap items-center justify-center gap-4">
|
||||
<div
|
||||
class="mt-10 flex flex-wrap items-center justify-center gap-4"
|
||||
>
|
||||
<template v-if="$page.props.auth.user">
|
||||
<Button size="lg" as-child>
|
||||
<Link :href="dashboard()">Accéder au tableau de bord</Link>
|
||||
<Link :href="dashboard()"
|
||||
>Accéder au tableau de bord</Link
|
||||
>
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Button size="lg" as-child>
|
||||
<Link :href="register()">Commencer gratuitement</Link>
|
||||
<Link :href="register()"
|
||||
>Commencer gratuitement</Link
|
||||
>
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" as-child>
|
||||
<Link :href="login()">Se connecter</Link>
|
||||
@@ -88,56 +109,71 @@ withDefaults(
|
||||
<!-- Features -->
|
||||
<section class="px-4 py-20">
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
<div class="mb-16 text-center">
|
||||
<h2
|
||||
class="text-3xl font-bold tracking-tight sm:text-4xl"
|
||||
>
|
||||
Tout ce dont votre cabinet a besoin
|
||||
</h2>
|
||||
<p class="mt-4 max-w-2xl mx-auto text-muted-foreground">
|
||||
Une solution complète pour gérer les échanges de documents fiscaux avec vos clients.
|
||||
<p class="mx-auto mt-4 max-w-2xl text-muted-foreground">
|
||||
Une solution complète pour gérer les échanges de
|
||||
documents fiscaux avec vos clients.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="mb-2 flex size-12 items-center justify-center rounded-lg bg-primary/10">
|
||||
<div
|
||||
class="mb-2 flex size-12 items-center justify-center rounded-lg bg-primary/10"
|
||||
>
|
||||
<Users class="size-6 text-primary" />
|
||||
</div>
|
||||
<CardTitle>Gestion des clients</CardTitle>
|
||||
<CardDescription>
|
||||
Gérez vos clients et leurs informations (ICE, IF, RC, CNSS…)
|
||||
Gérez vos clients et leurs informations
|
||||
(ICE, IF, RC, CNSS…)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="mb-2 flex size-12 items-center justify-center rounded-lg bg-primary/10">
|
||||
<div
|
||||
class="mb-2 flex size-12 items-center justify-center rounded-lg bg-primary/10"
|
||||
>
|
||||
<Calendar class="size-6 text-primary" />
|
||||
</div>
|
||||
<CardTitle>Dossiers par type</CardTitle>
|
||||
<CardTitle>Déclarations par type</CardTitle>
|
||||
<CardDescription>
|
||||
TVA, IS, IR, CNSS, Bilan annuel — créez et suivez vos dossiers
|
||||
TVA, IS, IR, CNSS, Bilan annuel — créez et
|
||||
suivez vos déclarations
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="mb-2 flex size-12 items-center justify-center rounded-lg bg-primary/10">
|
||||
<div
|
||||
class="mb-2 flex size-12 items-center justify-center rounded-lg bg-primary/10"
|
||||
>
|
||||
<Mail class="size-6 text-primary" />
|
||||
</div>
|
||||
<CardTitle>Invitations sécurisées</CardTitle>
|
||||
<CardDescription>
|
||||
Envoyez des liens par email pour que vos clients déposent leurs documents
|
||||
Envoyez des liens par email pour que vos
|
||||
clients déposent leurs documents
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="mb-2 flex size-12 items-center justify-center rounded-lg bg-primary/10">
|
||||
<div
|
||||
class="mb-2 flex size-12 items-center justify-center rounded-lg bg-primary/10"
|
||||
>
|
||||
<FileCheck class="size-6 text-primary" />
|
||||
</div>
|
||||
<CardTitle>Documents centralisés</CardTitle>
|
||||
<CardDescription>
|
||||
Tous les documents dans un espace unique, avec historique et téléchargement
|
||||
Tous les documents dans un espace unique,
|
||||
avec historique et téléchargement
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
@@ -154,12 +190,15 @@ withDefaults(
|
||||
</Card> -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="mb-2 flex size-12 items-center justify-center rounded-lg bg-primary/10">
|
||||
<div
|
||||
class="mb-2 flex size-12 items-center justify-center rounded-lg bg-primary/10"
|
||||
>
|
||||
<Shield class="size-6 text-primary" />
|
||||
</div>
|
||||
<CardTitle>Validation client</CardTitle>
|
||||
<CardDescription>
|
||||
Demandez une confirmation avec signature pour valider vos situations
|
||||
Demandez une confirmation avec signature
|
||||
pour valider vos situations
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
@@ -176,17 +215,22 @@ withDefaults(
|
||||
Prêt à simplifier votre quotidien ?
|
||||
</h2>
|
||||
<p class="mt-4 text-muted-foreground">
|
||||
Rejoignez les cabinets qui font confiance à {{ $page.props.name }}.
|
||||
Rejoignez les cabinets qui font confiance à
|
||||
{{ $page.props.name }}.
|
||||
</p>
|
||||
<div class="mt-8">
|
||||
<template v-if="$page.props.auth.user">
|
||||
<Button size="lg" as-child>
|
||||
<Link :href="dashboard()">Accéder au tableau de bord</Link>
|
||||
<Link :href="dashboard()"
|
||||
>Accéder au tableau de bord</Link
|
||||
>
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Button size="lg" as-child>
|
||||
<Link :href="register()">Créer un compte gratuit</Link>
|
||||
<Link :href="register()"
|
||||
>Créer un compte gratuit</Link
|
||||
>
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
@@ -197,13 +241,20 @@ withDefaults(
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-sidebar-border/70 py-8">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
<div class="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<div
|
||||
class="flex flex-col items-center justify-between gap-4 sm:flex-row"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<AppLogoIcon class="size-5 fill-current text-muted-foreground" />
|
||||
<span class="text-sm text-muted-foreground">{{ $page.props.name }}</span>
|
||||
<AppLogoIcon
|
||||
class="size-5 fill-current text-muted-foreground"
|
||||
/>
|
||||
<span class="text-sm text-muted-foreground">{{
|
||||
$page.props.name
|
||||
}}</span>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Tous droits réservés © {{ new Date().getFullYear() }} {{ $page.props.name }}.
|
||||
Tous droits réservés © {{ new Date().getFullYear() }}
|
||||
{{ $page.props.name }}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,14 +7,14 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
|
||||
type Folder = {
|
||||
type Declaration = {
|
||||
id: number;
|
||||
title: string;
|
||||
client_name: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
folder: Folder;
|
||||
declaration: Declaration;
|
||||
token: string;
|
||||
submitUrl: string;
|
||||
};
|
||||
@@ -27,31 +27,44 @@ const flash = computed(() => page.props.flash);
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-svh flex-col bg-background">
|
||||
<Head :title="`Confirmation - ${folder.title}`" />
|
||||
<Head :title="`Confirmation - ${declaration.title}`" />
|
||||
|
||||
<header class="border-b border-sidebar-border/70 px-4 py-4">
|
||||
<div class="mx-auto flex max-w-2xl items-center gap-3">
|
||||
<AppLogoIcon class="size-8 fill-current text-[var(--foreground)]" />
|
||||
<AppLogoIcon
|
||||
class="size-8 fill-current text-[var(--foreground)]"
|
||||
/>
|
||||
<div>
|
||||
<h1 class="font-medium">{{ folder.title }}</h1>
|
||||
<p class="text-sm text-muted-foreground">{{ folder.client_name }}</p>
|
||||
<h1 class="font-medium">{{ declaration.title }}</h1>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ declaration.client_name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto w-full max-w-2xl flex-1 p-4">
|
||||
<div v-if="flash?.message" class="mb-4 rounded-lg bg-green-100 p-3 text-sm text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||
<div
|
||||
v-if="flash?.message"
|
||||
class="mb-4 rounded-lg bg-green-100 p-3 text-sm text-green-800 dark:bg-green-900/30 dark:text-green-300"
|
||||
>
|
||||
{{ flash.message }}
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-medium">Confirmer la situation</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Veuillez signer ci-dessous pour confirmer la situation présentée par votre cabinet.
|
||||
Veuillez signer ci-dessous pour confirmer la situation
|
||||
présentée par votre cabinet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form :action="submitUrl" method="post" class="space-y-4" v-slot="{ processing }">
|
||||
<Form
|
||||
:action="submitUrl"
|
||||
method="post"
|
||||
class="space-y-4"
|
||||
v-slot="{ processing }"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<Label for="signature">Signature (nom complet)</Label>
|
||||
<Input
|
||||
|
||||
@@ -6,14 +6,14 @@ import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
|
||||
type Folder = {
|
||||
type Declaration = {
|
||||
id: number;
|
||||
title: string;
|
||||
client_name: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
folder: Folder;
|
||||
declaration: Declaration;
|
||||
token: string;
|
||||
submitUrl: string;
|
||||
};
|
||||
@@ -26,31 +26,44 @@ const flash = computed(() => page.props.flash);
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-svh flex-col bg-background">
|
||||
<Head :title="`Refus - ${folder.title}`" />
|
||||
<Head :title="`Refus - ${declaration.title}`" />
|
||||
|
||||
<header class="border-b border-sidebar-border/70 px-4 py-4">
|
||||
<div class="mx-auto flex max-w-2xl items-center gap-3">
|
||||
<AppLogoIcon class="size-8 fill-current text-[var(--foreground)]" />
|
||||
<AppLogoIcon
|
||||
class="size-8 fill-current text-[var(--foreground)]"
|
||||
/>
|
||||
<div>
|
||||
<h1 class="font-medium">{{ folder.title }}</h1>
|
||||
<p class="text-sm text-muted-foreground">{{ folder.client_name }}</p>
|
||||
<h1 class="font-medium">{{ declaration.title }}</h1>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ declaration.client_name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto w-full max-w-2xl flex-1 p-4">
|
||||
<div v-if="flash?.message" class="mb-4 rounded-lg bg-green-100 p-3 text-sm text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||
<div
|
||||
v-if="flash?.message"
|
||||
class="mb-4 rounded-lg bg-green-100 p-3 text-sm text-green-800 dark:bg-green-900/30 dark:text-green-300"
|
||||
>
|
||||
{{ flash.message }}
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-medium">Refuser la situation</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Vous pouvez indiquer la raison de votre refus (facultatif).
|
||||
Vous pouvez indiquer la raison de votre refus
|
||||
(facultatif).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form :action="submitUrl" method="post" class="space-y-4" v-slot="{ processing }">
|
||||
<Form
|
||||
:action="submitUrl"
|
||||
method="post"
|
||||
class="space-y-4"
|
||||
v-slot="{ processing }"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<Label for="reason">Raison du refus (facultatif)</Label>
|
||||
<textarea
|
||||
@@ -59,11 +72,15 @@ const flash = computed(() => page.props.flash);
|
||||
rows="4"
|
||||
placeholder="Précisez si besoin..."
|
||||
:disabled="processing"
|
||||
class="flex min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background 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"
|
||||
class="flex min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" variant="destructive" :disabled="processing">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
:disabled="processing"
|
||||
>
|
||||
<Spinner v-if="processing" class="mr-2 size-4" />
|
||||
Confirmer le refus
|
||||
</Button>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { ref, computed } from 'vue';
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
import { FileUp } from 'lucide-vue-next';
|
||||
import { ref, computed } from 'vue';
|
||||
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { FileUp } from 'lucide-vue-next';
|
||||
|
||||
type Folder = {
|
||||
type Declaration = {
|
||||
id: number;
|
||||
title: string;
|
||||
client_name: string;
|
||||
@@ -22,7 +22,7 @@ type Document = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
folder: Folder;
|
||||
declaration: Declaration;
|
||||
token: string;
|
||||
documents: Document[];
|
||||
uploadUrl: string;
|
||||
@@ -72,29 +72,35 @@ function submit() {
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-svh flex-col bg-background">
|
||||
<Head :title="`Dépôt - ${folder.title}`" />
|
||||
<Head :title="`Dépôt - ${declaration.title}`" />
|
||||
|
||||
<header class="border-b border-sidebar-border/70 px-4 py-4">
|
||||
<div class="mx-auto flex max-w-2xl items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<AppLogoIcon class="size-8 fill-current text-foreground" />
|
||||
<div>
|
||||
<h1 class="font-medium">{{ folder.title }}</h1>
|
||||
<p class="text-sm text-muted-foreground">{{ folder.client_name }}</p>
|
||||
<h1 class="font-medium">{{ declaration.title }}</h1>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ declaration.client_name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto w-full max-w-2xl flex-1 p-4">
|
||||
<div v-if="flash?.message" class="mb-4 rounded-lg bg-green-100 p-3 text-sm text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||
<div
|
||||
v-if="flash?.message"
|
||||
class="mb-4 rounded-lg bg-green-100 p-3 text-sm text-green-800 dark:bg-green-900/30 dark:text-green-300"
|
||||
>
|
||||
{{ flash.message }}
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-medium">Déposer vos documents</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Glissez-déposez vos fichiers ou cliquez pour sélectionner.
|
||||
Glissez-déposez vos fichiers ou cliquez pour
|
||||
sélectionner.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -129,23 +135,42 @@ function submit() {
|
||||
>
|
||||
<FileUp class="mx-auto size-12 text-muted-foreground" />
|
||||
<p class="mt-2 text-sm font-medium">
|
||||
{{ fileInput?.files?.length ? `${fileInput.files.length} fichier(s) sélectionné(s)` : 'Aucun fichier sélectionné' }}
|
||||
{{
|
||||
fileInput?.files?.length
|
||||
? `${fileInput.files.length} fichier(s) sélectionné(s)`
|
||||
: 'Aucun fichier sélectionné'
|
||||
}}
|
||||
</p>
|
||||
<Button type="button" variant="outline" class="mt-2" @click="triggerFileSelect">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="mt-2"
|
||||
@click="triggerFileSelect"
|
||||
>
|
||||
Choisir des fichiers
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button type="submit" :disabled="!fileInput?.files?.length || isSubmitting">
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="!fileInput?.files?.length || isSubmitting"
|
||||
>
|
||||
<Spinner v-if="isSubmitting" class="mr-2 size-4" />
|
||||
Envoyer
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div v-if="documents.length" class="rounded-xl border border-sidebar-border/70 p-4">
|
||||
<div
|
||||
v-if="documents.length"
|
||||
class="rounded-xl border border-sidebar-border/70 p-4"
|
||||
>
|
||||
<h3 class="font-medium">Documents déjà déposés</h3>
|
||||
<ul class="mt-2 space-y-1 text-sm text-muted-foreground">
|
||||
<li v-for="doc in documents" :key="doc.id" class="flex justify-between">
|
||||
<li
|
||||
v-for="doc in documents"
|
||||
:key="doc.id"
|
||||
class="flex justify-between"
|
||||
>
|
||||
<span>{{ doc.file_name }}</span>
|
||||
<span>{{ doc.size }} — {{ doc.created_at }}</span>
|
||||
</li>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Building2, FileText, FolderOpen } from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import DeclarationCalendar from '@/components/clients/DeclarationCalendar.vue';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import FolderCalendar from '@/components/clients/FolderCalendar.vue';
|
||||
import { Building2, FileText, FolderOpen } from 'lucide-vue-next';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
type ClientContact = {
|
||||
id: number;
|
||||
@@ -34,7 +34,7 @@ type Client = {
|
||||
internal_notes: string | null;
|
||||
};
|
||||
|
||||
type Folder = {
|
||||
type Declaration = {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
@@ -52,11 +52,11 @@ type Stats = {
|
||||
|
||||
type Props = {
|
||||
client: Client;
|
||||
folders: Folder[];
|
||||
declarations: Declaration[];
|
||||
stats: Stats;
|
||||
indexUrl: string;
|
||||
editUrl: string;
|
||||
createFolderUrl: string;
|
||||
createDeclarationUrl: string;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@@ -78,7 +78,7 @@ const typeLabels: Record<string, string> = {
|
||||
other: 'Autre',
|
||||
};
|
||||
|
||||
const folderStatusLabels: Record<string, string> = {
|
||||
const declarationStatusLabels: Record<string, string> = {
|
||||
draft: 'Brouillon',
|
||||
waiting_documents: 'En attente documents',
|
||||
documents_received: 'Documents reçus',
|
||||
@@ -139,7 +139,9 @@ function getFieldValue(fieldKey: string): string {
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" as-child>
|
||||
<Link :href="createFolderUrl">Nouveau dossier</Link>
|
||||
<Link :href="createDeclarationUrl"
|
||||
>Nouvelle déclaration</Link
|
||||
>
|
||||
</Button>
|
||||
<Button variant="outline" as-child>
|
||||
<Link :href="editUrl">Modifier le client</Link>
|
||||
@@ -154,7 +156,7 @@ function getFieldValue(fieldKey: string): string {
|
||||
class="flex flex-row items-center justify-between space-y-0 pb-2"
|
||||
>
|
||||
<CardTitle class="text-sm font-medium"
|
||||
>Total dossiers</CardTitle
|
||||
>Total déclarations</CardTitle
|
||||
>
|
||||
<FolderOpen class="size-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
@@ -181,8 +183,8 @@ function getFieldValue(fieldKey: string): string {
|
||||
?.additional_documents_requested ?? 0) +
|
||||
(stats.by_status?.waiting_documents ?? 0) +
|
||||
(stats.by_status?.documents_received ?? 0) +
|
||||
(stats.by_status
|
||||
?.waiting_client_validation ?? 0)
|
||||
(stats.by_status?.waiting_client_validation ??
|
||||
0)
|
||||
}}
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -384,7 +386,7 @@ function getFieldValue(fieldKey: string): string {
|
||||
>
|
||||
Notes
|
||||
</p>
|
||||
<p class="whitespace-pre-wrap text-sm">
|
||||
<p class="text-sm whitespace-pre-wrap">
|
||||
{{ client.internal_notes }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -398,55 +400,45 @@ function getFieldValue(fieldKey: string): string {
|
||||
<CardHeader>
|
||||
<CardTitle>Calendrier des échéances</CardTitle>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Dossiers par date limite
|
||||
Déclarations par date limite
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FolderCalendar :folders="folders" />
|
||||
<DeclarationCalendar :declarations="declarations" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Folders history -->
|
||||
<!-- Declarations history -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Historique des dossiers</CardTitle>
|
||||
<CardTitle>Historique des déclarations</CardTitle>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Derniers dossiers du client
|
||||
Dernières déclarations du client
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div
|
||||
v-if="folders.length"
|
||||
v-if="declarations.length"
|
||||
class="overflow-x-auto rounded-xl border border-sidebar-border/70"
|
||||
>
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/50">
|
||||
<tr>
|
||||
<th
|
||||
class="px-4 py-3 text-left font-medium"
|
||||
>
|
||||
Dossier
|
||||
<th class="px-4 py-3 text-left font-medium">
|
||||
Déclaration
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-left font-medium"
|
||||
>
|
||||
<th class="px-4 py-3 text-left font-medium">
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-left font-medium"
|
||||
>
|
||||
<th class="px-4 py-3 text-left font-medium">
|
||||
Statut
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-left font-medium"
|
||||
>
|
||||
<th class="px-4 py-3 text-left font-medium">
|
||||
Échéance
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-3 text-left font-medium"
|
||||
>
|
||||
<th class="px-4 py-3 text-left font-medium">
|
||||
Créé le
|
||||
</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
@@ -454,31 +446,31 @@ function getFieldValue(fieldKey: string): string {
|
||||
</thead>
|
||||
<tbody class="divide-y divide-sidebar-border/70">
|
||||
<tr
|
||||
v-for="folder in folders"
|
||||
:key="folder.id"
|
||||
v-for="declaration in declarations"
|
||||
:key="declaration.id"
|
||||
class="hover:bg-muted/30"
|
||||
>
|
||||
<td class="px-4 py-3 font-medium">
|
||||
{{ folder.title }}
|
||||
{{ declaration.title }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{
|
||||
typeLabels[folder.type] ??
|
||||
folder.type
|
||||
typeLabels[declaration.type] ??
|
||||
declaration.type
|
||||
}}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{
|
||||
folderStatusLabels[
|
||||
folder.status
|
||||
] ?? folder.status
|
||||
declarationStatusLabels[
|
||||
declaration.status
|
||||
] ?? declaration.status
|
||||
}}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{ folder.due_date ?? '—' }}
|
||||
{{ declaration.due_date ?? '—' }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{ folder.created_at }}
|
||||
{{ declaration.created_at }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<Button
|
||||
@@ -486,7 +478,7 @@ function getFieldValue(fieldKey: string): string {
|
||||
size="sm"
|
||||
as-child
|
||||
>
|
||||
<Link :href="folder.showUrl"
|
||||
<Link :href="declaration.showUrl"
|
||||
>Voir</Link
|
||||
>
|
||||
</Button>
|
||||
@@ -499,11 +491,11 @@ function getFieldValue(fieldKey: string): string {
|
||||
v-else
|
||||
class="rounded-xl border border-sidebar-border/70 p-8 text-center text-muted-foreground"
|
||||
>
|
||||
Aucun dossier.
|
||||
Aucune déclaration.
|
||||
<Link
|
||||
:href="createFolderUrl"
|
||||
:href="createDeclarationUrl"
|
||||
class="text-primary underline"
|
||||
>Créer un dossier</Link
|
||||
>Créer une déclaration</Link
|
||||
>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
import FolderForm from '@/components/FolderForm.vue';
|
||||
import type { FolderFormData } from '@/components/FolderForm.vue';
|
||||
import DeclarationForm from '@/components/DeclarationForm.vue';
|
||||
import type { DeclarationFormData } from '@/components/DeclarationForm.vue';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
@@ -20,9 +20,9 @@ type Props = {
|
||||
indexUrl: string;
|
||||
storeUrl: string;
|
||||
initialClientId?: number | null;
|
||||
folderTypeLabels: Record<string, string>;
|
||||
folderStatusLabels: Record<string, string>;
|
||||
folderPriorityLabels: Record<string, string>;
|
||||
declarationTypeLabels: Record<string, string>;
|
||||
declarationStatusLabels: Record<string, string>;
|
||||
declarationPriorityLabels: Record<string, string>;
|
||||
clients: Client[];
|
||||
workspaceUsers: WorkspaceUser[];
|
||||
};
|
||||
@@ -31,7 +31,7 @@ const props = defineProps<Props>();
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const form = useForm<FolderFormData>({
|
||||
const form = useForm<DeclarationFormData>({
|
||||
client_id: props.initialClientId ? String(props.initialClientId) : '',
|
||||
title: '',
|
||||
type: 'vat_monthly',
|
||||
@@ -54,36 +54,36 @@ function submit() {
|
||||
<template>
|
||||
<AppLayout
|
||||
:breadcrumbs="[
|
||||
{ title: 'Dossiers', href: props.indexUrl },
|
||||
{ title: 'Créer un dossier' },
|
||||
{ title: 'Déclarations', href: props.indexUrl },
|
||||
{ title: 'Nouvelle déclaration' },
|
||||
]"
|
||||
>
|
||||
<Head title="Créer un dossier" />
|
||||
<Head title="Nouvelle déclaration" />
|
||||
|
||||
<div class="flex flex-col space-y-6 p-4">
|
||||
<Heading
|
||||
title="Créer un dossier"
|
||||
description="Créer un nouveau dossier fiscal"
|
||||
title="Nouvelle déclaration"
|
||||
description="Créer une nouvelle déclaration fiscale"
|
||||
/>
|
||||
<div
|
||||
v-if="!props.clients.length"
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-200"
|
||||
>
|
||||
Aucun client dans ce workspace. Créez d'abord un
|
||||
Aucun client dans ce workspace. Créez d'abord un
|
||||
<Link href="/clients" class="font-medium underline">
|
||||
client
|
||||
</Link>
|
||||
pour pouvoir créer un dossier.
|
||||
pour pouvoir créer une déclaration.
|
||||
</div>
|
||||
<FolderForm
|
||||
<DeclarationForm
|
||||
v-else
|
||||
:form="form"
|
||||
:folder-type-labels="props.folderTypeLabels"
|
||||
:folder-status-labels="props.folderStatusLabels"
|
||||
:folder-priority-labels="props.folderPriorityLabels"
|
||||
:declaration-type-labels="props.declarationTypeLabels"
|
||||
:declaration-status-labels="props.declarationStatusLabels"
|
||||
:declaration-priority-labels="props.declarationPriorityLabels"
|
||||
:clients="props.clients"
|
||||
:workspace-users="props.workspaceUsers"
|
||||
submit-label="Créer le dossier"
|
||||
submit-label="Créer la déclaration"
|
||||
@submit="submit"
|
||||
/>
|
||||
</div>
|
||||
94
resources/js/pages/declarations/Edit.vue
Normal file
94
resources/js/pages/declarations/Edit.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import DeclarationForm from '@/components/DeclarationForm.vue';
|
||||
import type { DeclarationFormData } from '@/components/DeclarationForm.vue';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
type Client = {
|
||||
id: number;
|
||||
company_name: string;
|
||||
};
|
||||
|
||||
type WorkspaceUser = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
type Declaration = {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
client_id: number;
|
||||
period_year: number;
|
||||
period_month: number | null;
|
||||
period_quarter: number | null;
|
||||
due_date: string | null;
|
||||
status: string;
|
||||
priority: string | null;
|
||||
assigned_to: number | null;
|
||||
notes_internal: string | null;
|
||||
notes_client: string | null;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
declaration: Declaration;
|
||||
indexUrl: string;
|
||||
updateUrl: string;
|
||||
declarationTypeLabels: Record<string, string>;
|
||||
declarationStatusLabels: Record<string, string>;
|
||||
declarationPriorityLabels: Record<string, string>;
|
||||
clients: Client[];
|
||||
workspaceUsers: WorkspaceUser[];
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const form = useForm<DeclarationFormData>({
|
||||
client_id: props.declaration.client_id,
|
||||
title: props.declaration.title,
|
||||
type: props.declaration.type,
|
||||
period_year: props.declaration.period_year,
|
||||
period_month: props.declaration.period_month ?? '',
|
||||
period_quarter: props.declaration.period_quarter ?? '',
|
||||
due_date: props.declaration.due_date ?? '',
|
||||
status: props.declaration.status ?? 'draft',
|
||||
priority: props.declaration.priority ?? 'medium',
|
||||
assigned_to: props.declaration.assigned_to ?? '',
|
||||
notes_internal: props.declaration.notes_internal ?? '',
|
||||
notes_client: props.declaration.notes_client ?? '',
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.put(props.updateUrl);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout
|
||||
:breadcrumbs="[
|
||||
{ title: 'Déclarations', href: props.indexUrl },
|
||||
{ title: 'Modifier la déclaration' },
|
||||
]"
|
||||
>
|
||||
<Head :title="`Modifier ${props.declaration.title}`" />
|
||||
|
||||
<div class="flex flex-col space-y-6 p-4">
|
||||
<Heading
|
||||
:title="`Modifier ${props.declaration.title}`"
|
||||
description="Mettre à jour les informations de la déclaration"
|
||||
/>
|
||||
<DeclarationForm
|
||||
:form="form"
|
||||
:declaration-type-labels="props.declarationTypeLabels"
|
||||
:declaration-status-labels="props.declarationStatusLabels"
|
||||
:declaration-priority-labels="props.declarationPriorityLabels"
|
||||
:clients="props.clients"
|
||||
:workspace-users="props.workspaceUsers"
|
||||
submit-label="Enregistrer les modifications"
|
||||
@submit="submit"
|
||||
/>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -2,11 +2,11 @@
|
||||
import { Head, Link, router } from '@inertiajs/vue3';
|
||||
import { FolderOpen } from 'lucide-vue-next';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import Pagination from '@/components/Pagination.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
type Folder = {
|
||||
type Declaration = {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
@@ -34,20 +34,20 @@ type PaginatedData<T> = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
folders: PaginatedData<Folder>;
|
||||
declarations: PaginatedData<Declaration>;
|
||||
createUrl: string;
|
||||
workspaceName: string;
|
||||
};
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
function destroy(folder: Folder) {
|
||||
function destroy(declaration: Declaration) {
|
||||
if (
|
||||
window.confirm(
|
||||
`Êtes-vous sûr de vouloir supprimer « ${folder.title} » ?`,
|
||||
`Êtes-vous sûr de vouloir supprimer « ${declaration.title} » ?`,
|
||||
)
|
||||
) {
|
||||
router.delete(folder.destroyUrl);
|
||||
router.delete(declaration.destroyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,59 +76,57 @@ const statusLabels: Record<string, string> = {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout
|
||||
:breadcrumbs="[
|
||||
{ title: 'Dossiers' },
|
||||
]"
|
||||
>
|
||||
<Head title="Dossiers" />
|
||||
<AppLayout :breadcrumbs="[{ title: 'Déclarations' }]">
|
||||
<Head title="Déclarations" />
|
||||
|
||||
<div class="flex flex-col space-y-6 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<Heading
|
||||
variant="small"
|
||||
title="Dossiers"
|
||||
:description="`Gérer les dossiers du workspace « ${workspaceName} »`"
|
||||
title="Déclarations"
|
||||
:description="`Gérer les déclarations du workspace « ${workspaceName} »`"
|
||||
/>
|
||||
<Button as-child>
|
||||
<Link :href="createUrl">Créer un dossier</Link>
|
||||
<Link :href="createUrl">Nouvelle déclaration</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-xl border border-sidebar-border/70 dark:border-sidebar-border overflow-hidden"
|
||||
class="overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
|
||||
>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="border-b border-sidebar-border/70 bg-muted/50">
|
||||
<thead
|
||||
class="border-b border-sidebar-border/70 bg-muted/50"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium align-middle"
|
||||
class="h-10 px-4 text-left align-middle font-medium"
|
||||
>
|
||||
Titre
|
||||
</th>
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium align-middle"
|
||||
class="h-10 px-4 text-left align-middle font-medium"
|
||||
>
|
||||
Client
|
||||
</th>
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium align-middle"
|
||||
class="h-10 px-4 text-left align-middle font-medium"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium align-middle"
|
||||
class="h-10 px-4 text-left align-middle font-medium"
|
||||
>
|
||||
Statut
|
||||
</th>
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium align-middle"
|
||||
class="h-10 px-4 text-left align-middle font-medium"
|
||||
>
|
||||
Date limite
|
||||
</th>
|
||||
<th
|
||||
class="h-10 px-4 text-right font-medium align-middle"
|
||||
class="h-10 px-4 text-right align-middle font-medium"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
@@ -136,62 +134,80 @@ const statusLabels: Record<string, string> = {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="folder in folders.data"
|
||||
:key="folder.id"
|
||||
v-for="declaration in declarations.data"
|
||||
:key="declaration.id"
|
||||
class="border-b border-sidebar-border/50 last:border-0"
|
||||
>
|
||||
<td class="px-4 py-3 font-medium">
|
||||
<Link
|
||||
:href="folder.showUrl"
|
||||
:href="declaration.showUrl"
|
||||
class="hover:underline"
|
||||
>
|
||||
{{ folder.title }}
|
||||
{{ declaration.title }}
|
||||
</Link>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-muted-foreground">
|
||||
{{ folder.client_name }}
|
||||
{{ declaration.client_name }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-muted-foreground">
|
||||
{{ typeLabels[folder.type] ?? folder.type }}
|
||||
{{
|
||||
typeLabels[declaration.type] ??
|
||||
declaration.type
|
||||
}}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-muted-foreground">
|
||||
{{ statusLabels[folder.status] ?? folder.status }}
|
||||
{{
|
||||
statusLabels[declaration.status] ??
|
||||
declaration.status
|
||||
}}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-muted-foreground">
|
||||
{{ folder.due_date || '—' }}
|
||||
{{ declaration.due_date || '—' }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right space-x-2">
|
||||
<Button variant="outline" size="sm" as-child>
|
||||
<Link :href="folder.showUrl"
|
||||
<td class="space-x-2 px-4 py-3 text-right">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
as-child
|
||||
>
|
||||
<Link :href="declaration.showUrl"
|
||||
>Voir</Link
|
||||
>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" as-child>
|
||||
<Link :href="folder.editUrl"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
as-child
|
||||
>
|
||||
<Link :href="declaration.editUrl"
|
||||
>Modifier</Link
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
@click="destroy(folder)"
|
||||
@click="destroy(declaration)"
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!folders.data.length">
|
||||
<tr v-if="!declarations.data.length">
|
||||
<td
|
||||
colspan="6"
|
||||
class="px-4 py-8 text-center text-muted-foreground"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div
|
||||
class="flex flex-col items-center gap-2"
|
||||
>
|
||||
<FolderOpen class="h-10 w-10" />
|
||||
<p>Aucun dossier pour le moment.</p>
|
||||
<p>
|
||||
Aucune déclaration pour le moment.
|
||||
</p>
|
||||
<Button as-child>
|
||||
<Link :href="createUrl"
|
||||
>Créer votre premier
|
||||
dossier</Link
|
||||
>Créer votre première
|
||||
déclaration</Link
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -203,12 +219,12 @@ const statusLabels: Record<string, string> = {
|
||||
</div>
|
||||
<Pagination
|
||||
:pagination="{
|
||||
from: folders.from ?? 0,
|
||||
to: folders.to ?? 0,
|
||||
total: folders.total,
|
||||
current_page: folders.current_page,
|
||||
last_page: folders.last_page,
|
||||
per_page: folders.per_page,
|
||||
from: declarations.from ?? 0,
|
||||
to: declarations.to ?? 0,
|
||||
total: declarations.total,
|
||||
current_page: declarations.current_page,
|
||||
last_page: declarations.last_page,
|
||||
per_page: declarations.per_page,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head, Link, useForm } from '@inertiajs/vue3';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { CheckCircle2, Download, Paperclip, Send } from 'lucide-vue-next';
|
||||
import { computed, ref, watch, nextTick } from 'vue';
|
||||
import MessageBubble from '@/components/declarations/MessageBubble.vue';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Timeline } from '@/components/ui/timeline';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import MessageBubble from '@/components/folders/MessageBubble.vue';
|
||||
import { CheckCircle2, Download, Paperclip, Send } from 'lucide-vue-next';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Timeline } from '@/components/ui/timeline';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
type Folder = {
|
||||
type Declaration = {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
@@ -68,7 +68,7 @@ type WorkspaceUser = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
folder: Folder;
|
||||
declaration: Declaration;
|
||||
messages: Message[];
|
||||
documents: Document[];
|
||||
messagesStoreUrl: string;
|
||||
@@ -84,9 +84,12 @@ type Props = {
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const reactiveDocuments = ref(props.documents.map((d) => ({ ...d })));
|
||||
watch(() => props.documents, (newDocs) => {
|
||||
reactiveDocuments.value = newDocs.map((d) => ({ ...d }));
|
||||
});
|
||||
watch(
|
||||
() => props.documents,
|
||||
(newDocs) => {
|
||||
reactiveDocuments.value = newDocs.map((d) => ({ ...d }));
|
||||
},
|
||||
);
|
||||
|
||||
function onDocumentDownload(doc: Document & { is_downloaded: boolean }) {
|
||||
doc.is_downloaded = true;
|
||||
@@ -106,7 +109,11 @@ function submitMention() {
|
||||
|
||||
const tabFromUrl = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get('tab') === 'messages' ? 'messages' : params.get('tab') === 'documents' ? 'documents' : 'overview';
|
||||
return params.get('tab') === 'messages'
|
||||
? 'messages'
|
||||
: params.get('tab') === 'documents'
|
||||
? 'documents'
|
||||
: 'overview';
|
||||
};
|
||||
const tab = ref(tabFromUrl());
|
||||
watch(tab, (t) => {
|
||||
@@ -144,15 +151,18 @@ const priorityLabels: Record<string, string> = {
|
||||
high: 'Haute',
|
||||
};
|
||||
|
||||
function formatPeriod(folder: Folder): string {
|
||||
function formatPeriod(declaration: Declaration): string {
|
||||
const parts: string[] = [];
|
||||
if (folder.period_year) parts.push(String(folder.period_year));
|
||||
if (folder.period_quarter) parts.push(`T${folder.period_quarter}`);
|
||||
if (folder.period_month) parts.push(`M${folder.period_month}`);
|
||||
if (declaration.period_year) parts.push(String(declaration.period_year));
|
||||
if (declaration.period_quarter)
|
||||
parts.push(`T${declaration.period_quarter}`);
|
||||
if (declaration.period_month) parts.push(`M${declaration.period_month}`);
|
||||
return parts.join(' - ') || '—';
|
||||
}
|
||||
|
||||
function formatDateTime(iso: string | null): { date: string; time: string } | null {
|
||||
function formatDateTime(
|
||||
iso: string | null,
|
||||
): { date: string; time: string } | null {
|
||||
if (!iso) return null;
|
||||
const d = new Date(iso);
|
||||
return {
|
||||
@@ -217,13 +227,18 @@ function triggerDocumentsFileSelect() {
|
||||
function onDocumentsFilesChanged(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files?.length) {
|
||||
selectedDocuments.value = [...selectedDocuments.value, ...Array.from(input.files)];
|
||||
selectedDocuments.value = [
|
||||
...selectedDocuments.value,
|
||||
...Array.from(input.files),
|
||||
];
|
||||
syncDocumentsToInput();
|
||||
}
|
||||
}
|
||||
|
||||
function removeDocument(index: number) {
|
||||
selectedDocuments.value = selectedDocuments.value.filter((_, i) => i !== index);
|
||||
selectedDocuments.value = selectedDocuments.value.filter(
|
||||
(_, i) => i !== index,
|
||||
);
|
||||
syncDocumentsToInput();
|
||||
}
|
||||
|
||||
@@ -248,8 +263,8 @@ function autoResizeTextarea(e: Event) {
|
||||
|
||||
const messagesChronological = computed(() => [...props.messages].reverse());
|
||||
|
||||
const folderTimelineItems = computed(() => {
|
||||
const folder = props.folder;
|
||||
const declarationTimelineItems = computed(() => {
|
||||
const declaration = props.declaration;
|
||||
const items: Array<{
|
||||
title: string;
|
||||
date?: string;
|
||||
@@ -258,31 +273,51 @@ const folderTimelineItems = computed(() => {
|
||||
}> = [];
|
||||
|
||||
// Documents reçus
|
||||
const docsReceived =
|
||||
['documents_received', 'processing', 'additional_documents_requested', 'waiting_client_validation', 'validated', 'closed'].includes(
|
||||
folder.status,
|
||||
);
|
||||
const docsReceived = [
|
||||
'documents_received',
|
||||
'processing',
|
||||
'additional_documents_requested',
|
||||
'waiting_client_validation',
|
||||
'validated',
|
||||
'closed',
|
||||
].includes(declaration.status);
|
||||
items.push({
|
||||
title: docsReceived ? 'Documents reçus' : 'En attente des documents',
|
||||
state: docsReceived ? 'completed' : folder.status === 'waiting_documents' ? 'current' : 'pending',
|
||||
state: docsReceived
|
||||
? 'completed'
|
||||
: declaration.status === 'waiting_documents'
|
||||
? 'current'
|
||||
: 'pending',
|
||||
});
|
||||
|
||||
// Validation client
|
||||
const validatedFmt = formatDateTime(folder.validated_at);
|
||||
const validatedFmt = formatDateTime(declaration.validated_at);
|
||||
items.push({
|
||||
title: folder.validated_at ? 'Validé par le client' : 'Validation client',
|
||||
title: declaration.validated_at
|
||||
? 'Validé par le client'
|
||||
: 'Validation client',
|
||||
date: validatedFmt?.date,
|
||||
time: validatedFmt?.time,
|
||||
state: folder.validated_at ? 'completed' : folder.status === 'waiting_client_validation' ? 'current' : 'pending',
|
||||
state: declaration.validated_at
|
||||
? 'completed'
|
||||
: declaration.status === 'waiting_client_validation'
|
||||
? 'current'
|
||||
: 'pending',
|
||||
});
|
||||
|
||||
// Clôture
|
||||
const closedFmt = formatDateTime(folder.closed_at);
|
||||
const closedFmt = formatDateTime(declaration.closed_at);
|
||||
items.push({
|
||||
title: folder.closed_at ? 'Dossier clôturé' : 'Clôture du dossier',
|
||||
title: declaration.closed_at
|
||||
? 'Déclaration clôturée'
|
||||
: 'Clôture de la déclaration',
|
||||
date: closedFmt?.date,
|
||||
time: closedFmt?.time,
|
||||
state: folder.closed_at ? 'completed' : folder.status === 'closed' ? 'current' : 'pending',
|
||||
state: declaration.closed_at
|
||||
? 'completed'
|
||||
: declaration.status === 'closed'
|
||||
? 'current'
|
||||
: 'pending',
|
||||
});
|
||||
|
||||
return items;
|
||||
@@ -290,172 +325,313 @@ const folderTimelineItems = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="[
|
||||
{ title: 'Dossiers', href: props.indexUrl },
|
||||
{ title: props.folder.title },
|
||||
]">
|
||||
<AppLayout
|
||||
:breadcrumbs="[
|
||||
{ title: 'Déclarations', href: props.indexUrl },
|
||||
{ title: props.declaration.title },
|
||||
]"
|
||||
>
|
||||
<Head :title="props.declaration.title" />
|
||||
|
||||
<Head :title="props.folder.title" />
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex h-full flex-col">
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-sidebar-border/70 dark:border-sidebar-border p-4">
|
||||
<Heading variant="small" :title="props.folder.title"
|
||||
:description="typeLabels[folder.type] ?? folder.type" />
|
||||
class="flex items-center justify-between border-b border-sidebar-border/70 p-4 dark:border-sidebar-border"
|
||||
>
|
||||
<Heading
|
||||
variant="small"
|
||||
:title="props.declaration.title"
|
||||
:description="
|
||||
typeLabels[declaration.type] ?? declaration.type
|
||||
"
|
||||
/>
|
||||
<Button variant="outline" as-child>
|
||||
<Link :href="editUrl">Modifier le dossier</Link>
|
||||
<Link :href="editUrl">Modifier la déclaration</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs v-model="tab" class="h-full overflow-auto w-full flex-grow gap-0">
|
||||
<TabsList class="w-full rounded-none py-0 px-0 h-auto !bg-background">
|
||||
<TabsTrigger value="overview" class="border-0 py-2 border-b-2 !shadow-none rounded-none border-sidebar-border/70 dark:border-sidebar-border data-[state=active]:border-primary transition-all">
|
||||
<Tabs
|
||||
v-model="tab"
|
||||
class="h-full w-full flex-grow gap-0 overflow-auto"
|
||||
>
|
||||
<TabsList
|
||||
class="h-auto w-full rounded-none !bg-background px-0 py-0"
|
||||
>
|
||||
<TabsTrigger
|
||||
value="overview"
|
||||
class="rounded-none border-0 border-b-2 border-sidebar-border/70 py-2 !shadow-none transition-all data-[state=active]:border-primary dark:border-sidebar-border"
|
||||
>
|
||||
Aperçu
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="messages" class="border-0 py-2 border-b-2 !shadow-none rounded-none border-sidebar-border/70 dark:border-sidebar-border data-[state=active]:border-primary transition-all ">
|
||||
<TabsTrigger
|
||||
value="messages"
|
||||
class="rounded-none border-0 border-b-2 border-sidebar-border/70 py-2 !shadow-none transition-all data-[state=active]:border-primary dark:border-sidebar-border"
|
||||
>
|
||||
Messages
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="documents" class="border-0 py-2 border-b-2 !shadow-none rounded-none border-sidebar-border/70 dark:border-sidebar-border data-[state=active]:border-primary transition-all">
|
||||
<TabsTrigger
|
||||
value="documents"
|
||||
class="rounded-none border-0 border-b-2 border-sidebar-border/70 py-2 !shadow-none transition-all data-[state=active]:border-primary dark:border-sidebar-border"
|
||||
>
|
||||
Documents
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview" class="p-4">
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="col-span-8">
|
||||
<div class="rounded-xl border border-sidebar-border/70 dark:border-sidebar-border overflow-hidden">
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
|
||||
>
|
||||
<dl class="divide-y divide-sidebar-border/70">
|
||||
<div class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Client
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2">
|
||||
{{ folder.client_name || '—' }}
|
||||
{{ declaration.client_name || '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Type
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2">
|
||||
{{ typeLabels[folder.type] ?? folder.type }}
|
||||
{{
|
||||
typeLabels[declaration.type] ??
|
||||
declaration.type
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Période
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2">
|
||||
{{ formatPeriod(folder) }}
|
||||
{{ formatPeriod(declaration) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Date ouverture
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2">
|
||||
{{ folder.created_at ? new Date(folder.created_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }) : '—' }}
|
||||
{{
|
||||
declaration.created_at
|
||||
? new Date(
|
||||
declaration.created_at,
|
||||
).toLocaleDateString(
|
||||
'fr-FR',
|
||||
{
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
},
|
||||
)
|
||||
: '—'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Date limite
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2">
|
||||
{{ folder.due_date || '—' }}
|
||||
{{ declaration.due_date || '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Statut
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2">
|
||||
{{ statusLabels[folder.status] ?? folder.status }}
|
||||
{{
|
||||
statusLabels[
|
||||
declaration.status
|
||||
] ?? declaration.status
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Priorité
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2">
|
||||
{{ folder.priority ? (priorityLabels[folder.priority] ?? folder.priority) : '—' }}
|
||||
{{
|
||||
declaration.priority
|
||||
? (priorityLabels[
|
||||
declaration.priority
|
||||
] ?? declaration.priority)
|
||||
: '—'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Assigné à
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2">
|
||||
{{ folder.assignee_name || '—' }}
|
||||
{{
|
||||
declaration.assignee_name || '—'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Validé le
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2">
|
||||
{{ folder.validated_at || '—' }}
|
||||
{{
|
||||
declaration.validated_at || '—'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Clôturé le
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2">
|
||||
{{ folder.closed_at || '—' }}
|
||||
{{ declaration.closed_at || '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="folder.notes_internal"
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
v-if="declaration.notes_internal"
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Notes internes
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2 whitespace-pre-wrap">
|
||||
{{ folder.notes_internal }}
|
||||
<dd
|
||||
class="text-sm whitespace-pre-wrap sm:col-span-2"
|
||||
>
|
||||
{{ declaration.notes_internal }}
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="folder.notes_client"
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="text-sm font-medium text-muted-foreground">
|
||||
<div
|
||||
v-if="declaration.notes_client"
|
||||
class="flex flex-col gap-1 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4"
|
||||
>
|
||||
<dt
|
||||
class="text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
Notes client
|
||||
</dt>
|
||||
<dd class="text-sm sm:col-span-2 whitespace-pre-wrap">
|
||||
{{ folder.notes_client }}
|
||||
<dd
|
||||
class="text-sm whitespace-pre-wrap sm:col-span-2"
|
||||
>
|
||||
{{ declaration.notes_client }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div v-if="canMention" class="mt-4 rounded-xl border border-sidebar-border/70 dark:border-sidebar-border p-4">
|
||||
<h3 class="mb-3 text-sm font-medium">Notifier un collaborateur</h3>
|
||||
<form @submit.prevent="submitMention" class="space-y-3">
|
||||
<div
|
||||
v-if="canMention"
|
||||
class="mt-4 rounded-xl border border-sidebar-border/70 p-4 dark:border-sidebar-border"
|
||||
>
|
||||
<h3 class="mb-3 text-sm font-medium">
|
||||
Notifier un collaborateur
|
||||
</h3>
|
||||
<form
|
||||
@submit.prevent="submitMention"
|
||||
class="space-y-3"
|
||||
>
|
||||
<div>
|
||||
<Label for="mention-user" class="text-sm">Collaborateur</Label>
|
||||
<Label
|
||||
for="mention-user"
|
||||
class="text-sm"
|
||||
>Collaborateur</Label
|
||||
>
|
||||
<select
|
||||
id="mention-user"
|
||||
v-model="mentionForm.user_id"
|
||||
class="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>Sélectionner...</option>
|
||||
<option v-for="u in workspaceUsers" :key="u.id" :value="u.id">
|
||||
<option value="" disabled>
|
||||
Sélectionner...
|
||||
</option>
|
||||
<option
|
||||
v-for="u in workspaceUsers"
|
||||
:key="u.id"
|
||||
:value="u.id"
|
||||
>
|
||||
{{ u.name }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="mentionForm.errors.user_id" class="mt-1 text-xs text-destructive">{{ mentionForm.errors.user_id }}</p>
|
||||
<p
|
||||
v-if="mentionForm.errors.user_id"
|
||||
class="mt-1 text-xs text-destructive"
|
||||
>
|
||||
{{ mentionForm.errors.user_id }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="mention-message" class="text-sm">Message</Label>
|
||||
<Label
|
||||
for="mention-message"
|
||||
class="text-sm"
|
||||
>Message</Label
|
||||
>
|
||||
<textarea
|
||||
id="mention-message"
|
||||
v-model="mentionForm.message"
|
||||
rows="2"
|
||||
class="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
placeholder="Ex : Merci de traiter ce dossier en priorité"
|
||||
class="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
||||
placeholder="Ex : Merci de traiter cette déclaration en priorité"
|
||||
required
|
||||
maxlength="500"
|
||||
/>
|
||||
<p v-if="mentionForm.errors.message" class="mt-1 text-xs text-destructive">{{ mentionForm.errors.message }}</p>
|
||||
<p
|
||||
v-if="mentionForm.errors.message"
|
||||
class="mt-1 text-xs text-destructive"
|
||||
>
|
||||
{{ mentionForm.errors.message }}
|
||||
</p>
|
||||
</div>
|
||||
<Button type="submit" size="sm" :disabled="mentionForm.processing">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
:disabled="mentionForm.processing"
|
||||
>
|
||||
Envoyer la notification
|
||||
</Button>
|
||||
</form>
|
||||
@@ -463,22 +639,31 @@ const folderTimelineItems = computed(() => {
|
||||
</div>
|
||||
<div class="col-span-4">
|
||||
<Timeline
|
||||
:items="folderTimelineItems"
|
||||
class="rounded-xl border border-sidebar-border/70 dark:border-sidebar-border p-4"
|
||||
:items="declarationTimelineItems"
|
||||
class="rounded-xl border border-sidebar-border/70 p-4 dark:border-sidebar-border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="messages" class="flex flex-col min-h-0 p-0 h-full max-h-full relative">
|
||||
<TabsContent
|
||||
value="messages"
|
||||
class="relative flex h-full max-h-full min-h-0 flex-col p-0"
|
||||
>
|
||||
<div
|
||||
ref="messagesContainerRef"
|
||||
class="flex-1 overflow-y-auto overscroll-contain px-4 py-6 min-h-0 absolute top-0 left-0 right-0 bottom-0 overflow-hidden pb-24"
|
||||
class="absolute top-0 right-0 bottom-0 left-0 min-h-0 flex-1 overflow-hidden overflow-y-auto overscroll-contain px-4 py-6 pb-24"
|
||||
>
|
||||
<div v-if="messages.length" class="mx-auto max-w-3xl space-y-4">
|
||||
<div
|
||||
v-if="messages.length"
|
||||
class="mx-auto max-w-3xl space-y-4"
|
||||
>
|
||||
<MessageBubble
|
||||
v-for="msg in messagesChronological"
|
||||
:key="msg.id"
|
||||
:message="{ ...msg, attachments: msg.attachments ?? [] }"
|
||||
:message="{
|
||||
...msg,
|
||||
attachments: msg.attachments ?? [],
|
||||
}"
|
||||
:message-type-labels="messageTypeLabels"
|
||||
/>
|
||||
</div>
|
||||
@@ -486,10 +671,15 @@ const folderTimelineItems = computed(() => {
|
||||
v-else
|
||||
class="flex min-h-[200px] items-center justify-center text-center text-muted-foreground"
|
||||
>
|
||||
<p>Aucun message. Envoyez une invitation ou un message pour commencer.</p>
|
||||
<p>
|
||||
Aucun message. Envoyez une invitation ou un
|
||||
message pour commencer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0 px-4 pb-4 bg-gradient-to-t from-background to-transparent absolute bottom-0 w-full">
|
||||
<div
|
||||
class="absolute bottom-0 w-full shrink-0 bg-gradient-to-t from-background to-transparent px-4 pb-4"
|
||||
>
|
||||
<Form
|
||||
:action="messagesStoreUrl"
|
||||
method="post"
|
||||
@@ -521,7 +711,7 @@ const folderTimelineItems = computed(() => {
|
||||
{{ f.name }}
|
||||
<button
|
||||
type="button"
|
||||
class="hover:bg-muted-foreground/20 rounded p-0.5"
|
||||
class="rounded p-0.5 hover:bg-muted-foreground/20"
|
||||
@click.prevent="removeFile(i)"
|
||||
>
|
||||
×
|
||||
@@ -534,12 +724,20 @@ const folderTimelineItems = computed(() => {
|
||||
<select
|
||||
name="type"
|
||||
required
|
||||
class="mr-2 shrink-0 border-0 bg-transparent py-2 pr-6 text-sm text-muted-foreground focus:outline-none focus:ring-0"
|
||||
class="mr-2 shrink-0 border-0 bg-transparent py-2 pr-6 text-sm text-muted-foreground focus:ring-0 focus:outline-none"
|
||||
>
|
||||
<option value="invite">Invitation</option>
|
||||
<option value="situation">Situation</option>
|
||||
<option value="file_request">Demande de pièces</option>
|
||||
<option value="confirmation">Validation</option>
|
||||
<option value="invite">
|
||||
Invitation
|
||||
</option>
|
||||
<option value="situation">
|
||||
Situation
|
||||
</option>
|
||||
<option value="file_request">
|
||||
Demande de pièces
|
||||
</option>
|
||||
<option value="confirmation">
|
||||
Validation
|
||||
</option>
|
||||
<option value="text">Message</option>
|
||||
</select>
|
||||
<textarea
|
||||
@@ -547,7 +745,7 @@ const folderTimelineItems = computed(() => {
|
||||
required
|
||||
rows="1"
|
||||
placeholder="Écrire un message..."
|
||||
class="min-h-[24px] max-h-[200px] flex-1 resize-none overflow-hidden border-0 bg-transparent py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="max-h-[200px] min-h-[24px] flex-1 resize-none overflow-hidden border-0 bg-transparent py-2 text-sm placeholder:text-muted-foreground focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="processing"
|
||||
@input="autoResizeTextarea"
|
||||
/>
|
||||
@@ -566,7 +764,10 @@ const folderTimelineItems = computed(() => {
|
||||
class="shrink-0"
|
||||
:disabled="processing"
|
||||
>
|
||||
<Spinner v-if="processing" class="size-4" />
|
||||
<Spinner
|
||||
v-if="processing"
|
||||
class="size-4"
|
||||
/>
|
||||
<Send v-else class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -595,7 +796,7 @@ const folderTimelineItems = computed(() => {
|
||||
@change="onDocumentsFilesChanged"
|
||||
/>
|
||||
<div class="flex flex-wrap items-end gap-2">
|
||||
<div class="flex-1 space-y-2 min-w-[200px]">
|
||||
<div class="min-w-[200px] flex-1 space-y-2">
|
||||
<Label>Ajouter des fichiers</Label>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -610,9 +811,15 @@ const folderTimelineItems = computed(() => {
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="processing || selectedDocuments.length === 0"
|
||||
:disabled="
|
||||
processing ||
|
||||
selectedDocuments.length === 0
|
||||
"
|
||||
>
|
||||
<Spinner v-if="processing" class="mr-2 size-4" />
|
||||
<Spinner
|
||||
v-if="processing"
|
||||
class="mr-2 size-4"
|
||||
/>
|
||||
Télécharger
|
||||
</Button>
|
||||
</div>
|
||||
@@ -620,8 +827,11 @@ const folderTimelineItems = computed(() => {
|
||||
v-if="selectedDocuments.length"
|
||||
class="rounded-lg border border-sidebar-border/70 bg-muted/30 p-2"
|
||||
>
|
||||
<p class="mb-2 text-sm font-medium text-muted-foreground">
|
||||
{{ selectedDocuments.length }} fichier(s) à déposer
|
||||
<p
|
||||
class="mb-2 text-sm font-medium text-muted-foreground"
|
||||
>
|
||||
{{ selectedDocuments.length }} fichier(s) à
|
||||
déposer
|
||||
</p>
|
||||
<ul class="space-y-1.5">
|
||||
<li
|
||||
@@ -629,8 +839,12 @@ const folderTimelineItems = computed(() => {
|
||||
:key="`${file.name}-${i}`"
|
||||
class="flex items-center justify-between gap-2 rounded-md bg-background px-2 py-1.5 text-sm"
|
||||
>
|
||||
<span class="truncate">{{ file.name }}</span>
|
||||
<span class="flex shrink-0 items-center gap-2">
|
||||
<span class="truncate">{{
|
||||
file.name
|
||||
}}</span>
|
||||
<span
|
||||
class="flex shrink-0 items-center gap-2"
|
||||
>
|
||||
<span class="text-muted-foreground">
|
||||
{{ formatFileSize(file.size) }}
|
||||
</span>
|
||||
@@ -638,7 +852,9 @@ const folderTimelineItems = computed(() => {
|
||||
type="button"
|
||||
class="rounded p-0.5 hover:bg-muted-foreground/20"
|
||||
aria-label="Retirer"
|
||||
@click.prevent="removeDocument(i)"
|
||||
@click.prevent="
|
||||
removeDocument(i)
|
||||
"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -649,32 +865,74 @@ const folderTimelineItems = computed(() => {
|
||||
</Form>
|
||||
<div
|
||||
v-if="reactiveDocuments.length"
|
||||
class="rounded-xl border border-sidebar-border/70 overflow-hidden"
|
||||
class="overflow-hidden rounded-xl border border-sidebar-border/70"
|
||||
>
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-medium">Nom</th>
|
||||
<th class="px-4 py-2 text-left font-medium">Taille</th>
|
||||
<th class="px-4 py-2 text-left font-medium">Déposé par</th>
|
||||
<th class="px-4 py-2 text-left font-medium">Date</th>
|
||||
<th
|
||||
class="px-4 py-2 text-left font-medium"
|
||||
>
|
||||
Nom
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-2 text-left font-medium"
|
||||
>
|
||||
Taille
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-2 text-left font-medium"
|
||||
>
|
||||
Déposé par
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-2 text-left font-medium"
|
||||
>
|
||||
Date
|
||||
</th>
|
||||
<th class="px-4 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-sidebar-border/70">
|
||||
<tr v-for="doc in reactiveDocuments" :key="doc.id">
|
||||
<tbody
|
||||
class="divide-y divide-sidebar-border/70"
|
||||
>
|
||||
<tr
|
||||
v-for="doc in reactiveDocuments"
|
||||
:key="doc.id"
|
||||
>
|
||||
<td class="px-4 py-2">
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5"
|
||||
>
|
||||
{{ doc.file_name }}
|
||||
<CheckCircle2 v-if="doc.is_downloaded" class="size-3.5 text-green-500" />
|
||||
<CheckCircle2
|
||||
v-if="doc.is_downloaded"
|
||||
class="size-3.5 text-green-500"
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2">{{ doc.size }}</td>
|
||||
<td class="px-4 py-2">{{ doc.uploaded_by }}</td>
|
||||
<td class="px-4 py-2">{{ doc.created_at }}</td>
|
||||
<td class="px-4 py-2">
|
||||
<Button variant="ghost" size="sm" as-child>
|
||||
<a :href="doc.downloadUrl" download @click="onDocumentDownload(doc)">
|
||||
{{ doc.size }}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
{{ doc.uploaded_by }}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
{{ doc.created_at }}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
as-child
|
||||
>
|
||||
<a
|
||||
:href="doc.downloadUrl"
|
||||
download
|
||||
@click="
|
||||
onDocumentDownload(doc)
|
||||
"
|
||||
>
|
||||
<Download class="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
@@ -683,14 +941,15 @@ const folderTimelineItems = computed(() => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-if="!reactiveDocuments.length" class="rounded-xl border border-sidebar-border/70 p-8 text-center text-muted-foreground">
|
||||
<div
|
||||
v-if="!reactiveDocuments.length"
|
||||
class="rounded-xl border border-sidebar-border/70 p-8 text-center text-muted-foreground"
|
||||
>
|
||||
Aucun document. Ajoutez des fichiers ci-dessus.
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -1,94 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import FolderForm from '@/components/FolderForm.vue';
|
||||
import type { FolderFormData } from '@/components/FolderForm.vue';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
type Client = {
|
||||
id: number;
|
||||
company_name: string;
|
||||
};
|
||||
|
||||
type WorkspaceUser = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
type Folder = {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
client_id: number;
|
||||
period_year: number;
|
||||
period_month: number | null;
|
||||
period_quarter: number | null;
|
||||
due_date: string | null;
|
||||
status: string;
|
||||
priority: string | null;
|
||||
assigned_to: number | null;
|
||||
notes_internal: string | null;
|
||||
notes_client: string | null;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
folder: Folder;
|
||||
indexUrl: string;
|
||||
updateUrl: string;
|
||||
folderTypeLabels: Record<string, string>;
|
||||
folderStatusLabels: Record<string, string>;
|
||||
folderPriorityLabels: Record<string, string>;
|
||||
clients: Client[];
|
||||
workspaceUsers: WorkspaceUser[];
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const form = useForm<FolderFormData>({
|
||||
client_id: props.folder.client_id,
|
||||
title: props.folder.title,
|
||||
type: props.folder.type,
|
||||
period_year: props.folder.period_year,
|
||||
period_month: props.folder.period_month ?? '',
|
||||
period_quarter: props.folder.period_quarter ?? '',
|
||||
due_date: props.folder.due_date ?? '',
|
||||
status: props.folder.status ?? 'draft',
|
||||
priority: props.folder.priority ?? 'medium',
|
||||
assigned_to: props.folder.assigned_to ?? '',
|
||||
notes_internal: props.folder.notes_internal ?? '',
|
||||
notes_client: props.folder.notes_client ?? '',
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.put(props.updateUrl);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout
|
||||
:breadcrumbs="[
|
||||
{ title: 'Dossiers', href: props.indexUrl },
|
||||
{ title: 'Modifier le dossier' },
|
||||
]"
|
||||
>
|
||||
<Head :title="`Modifier ${props.folder.title}`" />
|
||||
|
||||
<div class="flex flex-col space-y-6 p-4">
|
||||
<Heading
|
||||
:title="`Modifier ${props.folder.title}`"
|
||||
description="Mettre à jour les informations du dossier"
|
||||
/>
|
||||
<FolderForm
|
||||
:form="form"
|
||||
:folder-type-labels="props.folderTypeLabels"
|
||||
:folder-status-labels="props.folderStatusLabels"
|
||||
:folder-priority-labels="props.folderPriorityLabels"
|
||||
:clients="props.clients"
|
||||
:workspace-users="props.workspaceUsers"
|
||||
submit-label="Enregistrer les modifications"
|
||||
@submit="submit"
|
||||
/>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -1,12 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { User, FolderOpen, Building2, Calendar, AlertCircle } from 'lucide-vue-next';
|
||||
import {
|
||||
User,
|
||||
FolderOpen,
|
||||
Building2,
|
||||
Calendar,
|
||||
AlertCircle,
|
||||
} from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
type WorkspaceUser = {
|
||||
id: number;
|
||||
@@ -24,10 +30,10 @@ type Workspace = {
|
||||
|
||||
type Stats = {
|
||||
clients: number;
|
||||
folders: number;
|
||||
folders_by_status: Record<string, number>;
|
||||
folders_this_month: number;
|
||||
folders_needing_attention: number;
|
||||
declarations: number;
|
||||
declarations_by_status: Record<string, number>;
|
||||
declarations_this_month: number;
|
||||
declarations_needing_attention: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -45,13 +51,14 @@ function roleLabel(role: string): string {
|
||||
|
||||
const inProgressCount = computed(
|
||||
() =>
|
||||
(props.stats.folders_by_status?.processing ?? 0) +
|
||||
(props.stats.folders_by_status?.additional_documents_requested ?? 0) +
|
||||
(props.stats.folders_by_status?.documents_received ?? 0),
|
||||
(props.stats.declarations_by_status?.processing ?? 0) +
|
||||
(props.stats.declarations_by_status?.additional_documents_requested ??
|
||||
0) +
|
||||
(props.stats.declarations_by_status?.documents_received ?? 0),
|
||||
);
|
||||
|
||||
const validatedCount = computed(
|
||||
() => props.stats.folders_by_status?.validated ?? 0,
|
||||
() => props.stats.declarations_by_status?.validated ?? 0,
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -77,85 +84,111 @@ const validatedCount = computed(
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-6">
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardHeader
|
||||
class="flex flex-row items-center justify-between space-y-0 pb-2"
|
||||
>
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Clients
|
||||
</CardTitle>
|
||||
<Building2 class="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ props.stats.clients }}</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ props.stats.clients }}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardHeader
|
||||
class="flex flex-row items-center justify-between space-y-0 pb-2"
|
||||
>
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Dossiers
|
||||
Déclarations
|
||||
</CardTitle>
|
||||
<FolderOpen class="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ props.stats.folders }}</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ props.stats.declarations }}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardHeader
|
||||
class="flex flex-row items-center justify-between space-y-0 pb-2"
|
||||
>
|
||||
<CardTitle class="text-sm font-medium">
|
||||
En cours
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ inProgressCount }}</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ inProgressCount }}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardHeader
|
||||
class="flex flex-row items-center justify-between space-y-0 pb-2"
|
||||
>
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Validés
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ validatedCount }}</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ validatedCount }}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardHeader
|
||||
class="flex flex-row items-center justify-between space-y-0 pb-2"
|
||||
>
|
||||
<CardTitle class="text-sm font-medium">
|
||||
Ce mois
|
||||
</CardTitle>
|
||||
<Calendar class="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ props.stats.folders_this_month }}</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ props.stats.declarations_this_month }}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardHeader
|
||||
class="flex flex-row items-center justify-between space-y-0 pb-2"
|
||||
>
|
||||
<CardTitle class="text-sm font-medium">
|
||||
À traiter
|
||||
</CardTitle>
|
||||
<AlertCircle class="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ props.stats.folders_needing_attention }}</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ props.stats.declarations_needing_attention }}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-xl border border-sidebar-border/70 dark:border-sidebar-border overflow-hidden"
|
||||
class="overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
|
||||
>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="border-b border-sidebar-border/70 bg-muted/50">
|
||||
<thead
|
||||
class="border-b border-sidebar-border/70 bg-muted/50"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium align-middle"
|
||||
class="h-10 px-4 text-left align-middle font-medium"
|
||||
>
|
||||
User
|
||||
</th>
|
||||
<th
|
||||
class="h-10 px-4 text-left font-medium align-middle"
|
||||
class="h-10 px-4 text-left align-middle font-medium"
|
||||
>
|
||||
Role
|
||||
</th>
|
||||
@@ -189,7 +222,9 @@ const validatedCount = computed(
|
||||
colspan="2"
|
||||
class="px-4 py-8 text-center text-muted-foreground"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div
|
||||
class="flex flex-col items-center gap-2"
|
||||
>
|
||||
<User class="h-10 w-10" />
|
||||
<p>No users in this workspace.</p>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Bonjour,
|
||||
|
||||
Votre cabinet comptable vous demande de valider la situation pour le dossier **{{ $folderTitle }}**.
|
||||
Votre cabinet comptable vous demande de valider la situation pour le dossier **{{ $declarationTitle }}**.
|
||||
|
||||
{{ $body }}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Bonjour,
|
||||
|
||||
Votre cabinet comptable vous demande des documents complémentaires pour le dossier **{{ $folderTitle }}**.
|
||||
Votre cabinet comptable vous demande des documents complémentaires pour le dossier **{{ $declarationTitle }}**.
|
||||
|
||||
{{ $body }}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Bonjour,
|
||||
|
||||
Votre cabinet comptable vous invite à déposer les documents nécessaires pour le dossier **{{ $folderTitle }}**.
|
||||
Votre cabinet comptable vous invite à déposer les documents nécessaires pour le dossier **{{ $declarationTitle }}**.
|
||||
|
||||
Cliquez sur le bouton ci-dessous pour accéder à l'interface sécurisée de dépôt de documents.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Bonjour,
|
||||
|
||||
**{{ $mentionedByName }}** vous a mentionné sur le dossier **{{ $folderTitle }}**.
|
||||
**{{ $mentionedByName }}** vous a mentionné sur le dossier **{{ $declarationTitle }}**.
|
||||
|
||||
> {{ $message }}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Bonjour,
|
||||
|
||||
Votre cabinet comptable a mis à jour la situation pour le dossier **{{ $folderTitle }}**.
|
||||
Votre cabinet comptable a mis à jour la situation pour le dossier **{{ $declarationTitle }}**.
|
||||
|
||||
{{ $body }}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Bonjour,
|
||||
|
||||
Vous avez reçu un nouveau message pour le dossier **{{ $folderTitle }}**.
|
||||
Vous avez reçu un nouveau message pour le dossier **{{ $declarationTitle }}**.
|
||||
|
||||
> {{ $body }}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
body,
|
||||
body *:not(html):not(style):not(br):not(tr):not(code) {
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -141,7 +142,9 @@ img {
|
||||
border-color: #e4e4e7;
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||
0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: 570px;
|
||||
|
||||
Reference in New Issue
Block a user