Files
L-Ami-Fiduciaire/resources/js/components/folders/MessageBubble.vue
Saad Zoubir 7a18c40361 chore: add BMAD framework modules, folder features, and tooling configs
Includes BMAD bmb/bmm/cis/tea workflow modules, folder (declaration)
feature implementation (controllers, models, enums, views, tests),
claude/cursor command configs, and email templates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:24:17 +01: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>