Files
L-Ami-Fiduciaire/resources/js/components/folders/MessageBubble.vue
Saad Ibn-Ezzoubayr 35545c2a8f feat: L'Ami Fiduciaire V1.0.0 — full codebase with Story 0.1 complete
Initial commit of the L'Ami Fiduciaire SaaS platform built on Laravel 12,
Vue 3, Inertia.js 2, and Tailwind CSS 4.

Story 0.1 (rename folders to declarations in database) is implemented and
code-reviewed: migration, rollback, and 6 Pest tests all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 23:33:10 +00:00

137 lines
5.0 KiB
Vue

<script setup lang="ts">
import { Button } from '@/components/ui/button';
import { CheckCircle2, Download, FileText } from 'lucide-vue-next';
type Props = {
message: {
id: number;
type: string;
body: string;
sent_by_type: string;
sender_name: string;
created_at: string;
attachments?: Array<{
id: number;
file_name: string;
mime_type?: string;
size: string;
downloadUrl: string;
is_downloaded?: boolean;
}>;
confirmation_status?: 'pending' | 'confirmed' | 'refused' | null;
};
messageTypeLabels: Record<string, string>;
};
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',
text: 'bg-slate-100 text-slate-700 dark:bg-slate-800/50 dark:text-slate-300',
};
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',
},
confirmed: {
label: 'Validé',
class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
},
refused: {
label: 'Refusé',
class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
},
};
function isImageMime(mime?: string | null): boolean {
return mime?.startsWith('image/') ?? false;
}
function getTypeColor(type: string): string {
return typeColors[type] ?? 'bg-muted text-muted-foreground';
}
</script>
<template>
<div
class="rounded-2xl px-4 py-3"
:class="
message.sent_by_type === 'user'
? 'ml-auto max-w-[85%] bg-muted'
: 'mr-auto max-w-[85%] bg-muted/70'
"
>
<div class="flex flex-wrap items-center justify-between gap-2 text-sm">
<span class="font-medium">{{ message.sender_name }}</span>
<span class="text-muted-foreground">{{ message.created_at }}</span>
</div>
<div class="mt-1 flex flex-wrap items-center gap-2">
<span
class="inline-flex rounded px-2 py-0.5 text-xs font-medium"
:class="getTypeColor(message.type)"
>
{{ messageTypeLabels[message.type] ?? message.type }}
</span>
<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 ?? ''"
>
{{ confirmationStatusLabels[message.confirmation_status]?.label ?? message.confirmation_status }}
</span>
</div>
<p class="mt-2 whitespace-pre-wrap text-sm">{{ message.body }}</p>
<div
v-if="message.attachments?.length"
class="mt-3 flex flex-wrap gap-2"
>
<div
v-for="att in message.attachments"
: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">
<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"
/>
</div>
<div class="min-w-0 flex-1">
<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" />
</p>
<p class="text-xs text-muted-foreground">{{ att.size }}</p>
</div>
<Button
variant="ghost"
size="sm"
as-child
class="shrink-0"
>
<a
:href="att.downloadUrl"
target="_blank"
rel="noopener"
download
@click="att.is_downloaded = true"
>
<Download class="size-4" />
</a>
</Button>
</div>
</div>
</div>
</template>