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:
147
resources/js/components/declarations/MessageBubble.vue
Normal file
147
resources/js/components/declarations/MessageBubble.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckCircle2, Download, FileText } from 'lucide-vue-next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
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 text-sm whitespace-pre-wrap">{{ 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>
|
||||
Reference in New Issue
Block a user