Add price list management across the API, store, services, routes, navigation, and sales views. Support quotes for either a client or a client group, including PDF download and nullable client validation for group-based recipients. Extend client groups to manage assigned clients directly from the form and detail views, and refresh supplier, intervention, stock, and order screens with updated interactions and layouts.
1057 lines
39 KiB
Vue
1057 lines
39 KiB
Vue
<template>
|
|
<div class="intervention-page">
|
|
<!-- Top Navigation Bar -->
|
|
<div class="page-topbar">
|
|
<router-link to="/interventions" class="back-btn">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<path d="M19 12H5M12 5l-7 7 7 7"/>
|
|
</svg>
|
|
Interventions
|
|
</router-link>
|
|
|
|
<div class="topbar-center" v-if="mappedIntervention">
|
|
<span class="topbar-id">#{{ mappedIntervention.id }}</span>
|
|
<StatusPill :status="mappedIntervention.status" />
|
|
</div>
|
|
|
|
<div class="topbar-actions">
|
|
<button class="action-btn secondary" @click="$emit('cancel')">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
Annuler
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading -->
|
|
<div v-if="loading" class="fullpage-loading">
|
|
<div class="loading-orb"></div>
|
|
<p>Chargement de l'intervention…</p>
|
|
</div>
|
|
|
|
<!-- Error -->
|
|
<div v-else-if="error" class="fullpage-error">
|
|
<div class="error-icon">
|
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
|
</div>
|
|
<h3>Erreur de chargement</h3>
|
|
<p>{{ error }}</p>
|
|
<button class="action-btn primary" @click="$emit('cancel')">Retour</button>
|
|
</div>
|
|
|
|
<!-- Main Layout -->
|
|
<div v-else-if="mappedIntervention" class="page-layout">
|
|
|
|
<!-- LEFT SIDEBAR -->
|
|
<aside class="sidebar">
|
|
<!-- Hero Card -->
|
|
<div class="hero-card">
|
|
<div class="hero-avatar">
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/>
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
</svg>
|
|
</div>
|
|
<div class="hero-info">
|
|
<h2 class="hero-name">{{ mappedIntervention.defuntName }}</h2>
|
|
<p class="hero-type">{{ mappedIntervention.title }}</p>
|
|
</div>
|
|
<StatusPill :status="mappedIntervention.status" large />
|
|
</div>
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="quick-stats">
|
|
<div class="stat-item">
|
|
<div class="stat-icon date-icon">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
|
</div>
|
|
<div>
|
|
<div class="stat-label">Date</div>
|
|
<div class="stat-value">{{ mappedIntervention.date }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-icon loc-icon">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
|
</div>
|
|
<div>
|
|
<div class="stat-label">Lieu</div>
|
|
<div class="stat-value">{{ mappedIntervention.lieux }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-icon dur-icon">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
</div>
|
|
<div>
|
|
<div class="stat-label">Durée</div>
|
|
<div class="stat-value">{{ mappedIntervention.duree }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Navigation -->
|
|
<nav class="sidebar-nav">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.id"
|
|
class="nav-item"
|
|
:class="{ active: activeTab === tab.id }"
|
|
@click="$emit('change-tab', tab.id)"
|
|
>
|
|
<span class="nav-icon" v-html="tab.icon"></span>
|
|
<span class="nav-label">{{ tab.label }}</span>
|
|
<span v-if="tab.badge" class="nav-badge">{{ tab.badge }}</span>
|
|
</button>
|
|
</nav>
|
|
|
|
<!-- Assign Button -->
|
|
<button class="assign-btn" @click="$emit('assign-practitioner')">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
|
|
Assigner un praticien
|
|
</button>
|
|
</aside>
|
|
|
|
<!-- MAIN CONTENT -->
|
|
<main class="main-content">
|
|
|
|
<!-- ── OVERVIEW TAB ── -->
|
|
<div v-if="activeTab === 'overview'" class="tab-content">
|
|
<div class="section-header">
|
|
<h3>Vue d'ensemble</h3>
|
|
</div>
|
|
<div class="info-grid">
|
|
<InfoSection title="Informations générales" icon-color="#6366f1">
|
|
<template #icon>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M6 20v-2a6 6 0 0 1 12 0v2"/></svg>
|
|
</template>
|
|
<DataRow label="Nom du défunt" :value="mappedIntervention.defuntName" />
|
|
<DataRow label="Date prévue" :value="mappedIntervention.date" />
|
|
<DataRow label="Lieu" :value="mappedIntervention.lieux" />
|
|
<DataRow label="Durée" :value="mappedIntervention.duree" />
|
|
</InfoSection>
|
|
|
|
<InfoSection title="Contact & Communication" icon-color="#10b981">
|
|
<template #icon>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 9.91a16 16 0 0 0 6.16 6.16l.91-.91a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 17z"/></svg>
|
|
</template>
|
|
<DataRow label="Contact familial" :value="mappedIntervention.contactFamilial" />
|
|
<DataRow label="Coordonnées" :value="mappedIntervention.coordonneesContact" />
|
|
<DataRow label="Type d'intervention" :value="mappedIntervention.title" />
|
|
</InfoSection>
|
|
|
|
<InfoSection title="Informations supplémentaires" icon-color="#f59e0b" class="full-width">
|
|
<template #icon>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
</template>
|
|
<div class="row-2col">
|
|
<DataRow label="Personnes attendues" :value="String(mappedIntervention.nombrePersonnes || '-')" />
|
|
<DataRow label="Prestations supp." :value="mappedIntervention.prestationsSupplementaires" />
|
|
</div>
|
|
</InfoSection>
|
|
|
|
<InfoSection title="Description" icon-color="#8b5cf6" class="full-width">
|
|
<template #icon>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
|
</template>
|
|
<p class="description-text">{{ mappedIntervention.description || 'Aucune description disponible.' }}</p>
|
|
</InfoSection>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── DETAILS TAB ── -->
|
|
<div v-if="activeTab === 'details'" class="tab-content">
|
|
<div class="section-header">
|
|
<h3>Détails</h3>
|
|
</div>
|
|
<InterventionDetails
|
|
:intervention="mappedIntervention"
|
|
:loading="loading"
|
|
:error="error"
|
|
@update="$emit('update-intervention', $event)"
|
|
@cancel="$emit('cancel')"
|
|
/>
|
|
</div>
|
|
|
|
<!-- ── TEAM TAB ── -->
|
|
<div v-if="activeTab === 'team'" class="tab-content">
|
|
<div class="section-header">
|
|
<h3>Équipe assignée</h3>
|
|
<button class="action-btn primary sm" @click="$emit('assign-practitioner')">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
Ajouter
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="mappedIntervention.practitioners?.length" class="team-grid">
|
|
<div
|
|
v-for="(p, i) in mappedIntervention.practitioners"
|
|
:key="i"
|
|
class="practitioner-card"
|
|
>
|
|
<div class="pract-avatar">{{ getInitials(p.name) }}</div>
|
|
<div class="pract-info">
|
|
<div class="pract-name">{{ p.name }}</div>
|
|
<div class="pract-role">
|
|
<span class="role-badge" :class="p.role === 'principal' ? 'role-principal' : 'role-assistant'">
|
|
{{ p.role === 'principal' ? 'Principal' : 'Assistant' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button class="unassign-btn" @click="$emit('unassign-practitioner', { practitionerId: p.id, practitionerName: p.name })">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state">
|
|
<div class="empty-icon">
|
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="23" y1="11" x2="17" y2="11"/><line x1="20" y1="8" x2="20" y2="14"/></svg>
|
|
</div>
|
|
<p>Aucun praticien assigné</p>
|
|
<button class="action-btn primary sm" @click="$emit('assign-practitioner')">Assigner maintenant</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── DOCUMENTS TAB ── -->
|
|
<div v-if="activeTab === 'documents'" class="tab-content">
|
|
<div class="section-header">
|
|
<h3>Documents</h3>
|
|
</div>
|
|
<DocumentManagement
|
|
:documents="documentAttachments"
|
|
:loading="documentStore.isLoading"
|
|
:error="documentStore.getError"
|
|
@files-selected="handleFilesSelected"
|
|
@upload-files="handleUploadFiles"
|
|
@delete-document="handleDeleteDocument"
|
|
@delete-documents="handleDeleteDocuments"
|
|
@update-document-label="handleUpdateDocumentLabel"
|
|
@retry="loadDocumentAttachments"
|
|
/>
|
|
</div>
|
|
|
|
<!-- ── QUOTE TAB ── -->
|
|
<div v-if="activeTab === 'quote'" class="tab-content">
|
|
<div class="section-header">
|
|
<h3>Devis</h3>
|
|
<router-link
|
|
v-if="mappedIntervention.quote?.id"
|
|
:to="`/ventes/devis/${mappedIntervention.quote.id}`"
|
|
class="action-btn primary sm"
|
|
>Ouvrir le devis</router-link>
|
|
</div>
|
|
|
|
<div v-if="mappedIntervention.quote">
|
|
<div class="info-grid">
|
|
<InfoSection title="Informations du devis" icon-color="#3b82f6">
|
|
<template #icon><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></template>
|
|
<DataRow label="Référence" :value="mappedIntervention.quote.reference" />
|
|
<DataRow label="Date" :value="mappedIntervention.quote.quote_date" />
|
|
<DataRow label="Validité" :value="mappedIntervention.quote.valid_until" />
|
|
<div class="data-row">
|
|
<span class="data-label">Statut</span>
|
|
<span class="quote-status-badge" :class="`qs-${getQuoteStatusColor(mappedIntervention.quote.status)}`">
|
|
{{ getQuoteStatusLabel(mappedIntervention.quote.status) }}
|
|
</span>
|
|
</div>
|
|
</InfoSection>
|
|
|
|
<InfoSection title="Montants" icon-color="#10b981">
|
|
<template #icon><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></template>
|
|
<DataRow label="Total HT" :value="formatCurrency(mappedIntervention.quote.total_ht)" />
|
|
<DataRow label="Total TVA" :value="formatCurrency(mappedIntervention.quote.total_tva)" />
|
|
<DataRow label="Total TTC" :value="formatCurrency(mappedIntervention.quote.total_ttc)" bold />
|
|
</InfoSection>
|
|
</div>
|
|
|
|
<div class="quote-lines-section" v-if="mappedIntervention.quote.lines?.length">
|
|
<div class="quote-table-header">Lignes du devis</div>
|
|
<div class="quote-table-wrapper">
|
|
<table class="quote-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Description</th>
|
|
<th class="text-center">Qté</th>
|
|
<th class="text-right">Prix unitaire</th>
|
|
<th class="text-right">Total HT</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="line in mappedIntervention.quote.lines" :key="line.id">
|
|
<td>{{ line.description || '-' }}</td>
|
|
<td class="text-center">{{ line.units_qty || 0 }}</td>
|
|
<td class="text-right">{{ formatCurrency(line.unit_price) }}</td>
|
|
<td class="text-right fw-semibold">{{ formatCurrency(line.total_ht) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state">
|
|
<div class="empty-icon">
|
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
|
</div>
|
|
<p>Aucun devis associé à cette intervention</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── HISTORY TAB ── -->
|
|
<div v-if="activeTab === 'history'" class="tab-content">
|
|
<div class="section-header"><h3>Historique</h3></div>
|
|
<div class="empty-state">
|
|
<div class="empty-icon">
|
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.5"/></svg>
|
|
</div>
|
|
<p>Historique des modifications</p>
|
|
<span class="coming-soon">Fonctionnalité à venir</span>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Assign Modal -->
|
|
<AssignPractitionerModal
|
|
:is-open="isModalOpen"
|
|
@close="$emit('close-modal')"
|
|
@assign="$emit('assign-practitioner-confirmed', $event)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { defineProps, defineEmits, ref, computed, watch, onMounted } from 'vue';
|
|
import { RouterLink } from 'vue-router';
|
|
import InterventionDetails from '@/components/molecules/Interventions/interventionDetails.vue';
|
|
import DocumentManagement from '@/components/molecules/Interventions/DocumentManagement.vue';
|
|
import AssignPractitionerModal from '@/components/molecules/intervention/AssignPractitionerModal.vue';
|
|
import { useDocumentAttachmentStore } from '@/stores/documentAttachmentStore';
|
|
|
|
// ── Sub-components (inline for self-containment) ──
|
|
const StatusPill = {
|
|
props: { status: Object, large: Boolean },
|
|
template: `
|
|
<span class="status-pill" :class="['sp-' + (status?.color || 'secondary'), large ? 'sp-lg' : '']">
|
|
{{ status?.label || 'En attente' }}
|
|
</span>
|
|
`
|
|
};
|
|
|
|
const InfoSection = {
|
|
props: { title: String, iconColor: String },
|
|
template: `
|
|
<div class="info-section">
|
|
<div class="info-section-header" :style="{ '--accent': iconColor }">
|
|
<span class="info-section-icon"><slot name="icon" /></span>
|
|
<span class="info-section-title">{{ title }}</span>
|
|
</div>
|
|
<div class="info-section-body"><slot /></div>
|
|
</div>
|
|
`
|
|
};
|
|
|
|
const DataRow = {
|
|
props: { label: String, value: String, bold: Boolean },
|
|
template: `
|
|
<div class="data-row">
|
|
<span class="data-label">{{ label }}</span>
|
|
<span class="data-value" :class="{ 'fw-semibold': bold }">{{ value || '-' }}</span>
|
|
</div>
|
|
`
|
|
};
|
|
|
|
const props = defineProps({
|
|
intervention: { type: Object, required: true },
|
|
loading: { type: Boolean, default: false },
|
|
error: { type: String, default: null },
|
|
activeTab: { type: String, default: 'overview' },
|
|
practitioners: { type: Array, default: () => [] },
|
|
isModalOpen: { type: Boolean, default: false },
|
|
});
|
|
|
|
const emit = defineEmits([
|
|
'update-intervention', 'cancel', 'assign-practitioner',
|
|
'assign-practitioner-confirmed', 'change-tab', 'close-modal',
|
|
'unassign-practitioner',
|
|
]);
|
|
|
|
const documentStore = useDocumentAttachmentStore();
|
|
const documentAttachments = computed(() =>
|
|
documentStore.getInterventionAttachments(props.intervention?.id || 0)
|
|
);
|
|
|
|
// ── Mapped intervention ──
|
|
const mappedIntervention = computed(() => {
|
|
if (!props.intervention) return null;
|
|
const i = props.intervention;
|
|
return {
|
|
...i,
|
|
defuntName: i.deceased
|
|
? `${i.deceased.last_name || ''} ${i.deceased.first_name || ''}`.trim()
|
|
: `Personne ${i.deceased_id || 'inconnue'}`,
|
|
date: i.scheduled_at ? new Date(i.scheduled_at).toLocaleString('fr-FR') : 'Non définie',
|
|
lieux: i.location?.name || 'Lieu non défini',
|
|
duree: i.duration_min ? `${i.duration_min} min` : 'Non définie',
|
|
title: i.type ? getTypeLabel(i.type) : 'Type non défini',
|
|
contactFamilial: i.order_giver || 'Non renseigné',
|
|
description: i.notes || '',
|
|
nombrePersonnes: i.attachments_count || 0,
|
|
coordonneesContact: i.client ? (i.client.email || i.client.phone || 'Non disponible') : 'Non disponible',
|
|
prestationsSupplementaires: 'À définir',
|
|
practitioners: (i.practitioners || []).map(p => ({
|
|
id: p.id,
|
|
name: p.employee
|
|
? `${p.employee.first_name || ''} ${p.employee.last_name || ''}`.trim()
|
|
: `${p.first_name || ''} ${p.last_name || ''}`.trim(),
|
|
role: p.pivot?.role || 'assistant',
|
|
})),
|
|
status: i.status
|
|
? { label: getStatusLabel(i.status), color: getStatusColor(i.status) }
|
|
: { label: 'En attente', color: 'warning' },
|
|
quote: i.quote || null,
|
|
};
|
|
});
|
|
|
|
const tabs = computed(() => [
|
|
{ id: 'overview', label: 'Vue d\'ensemble', icon: eyeIcon },
|
|
{ id: 'details', label: 'Détails', icon: listIcon },
|
|
{ id: 'team', label: 'Équipe', icon: teamIcon, badge: mappedIntervention.value?.practitioners?.length || null },
|
|
{ id: 'documents', label: 'Documents', icon: docIcon },
|
|
{ id: 'quote', label: 'Devis', icon: quoteIcon },
|
|
{ id: 'history', label: 'Historique', icon: histIcon },
|
|
]);
|
|
|
|
// SVG icons as strings for tab nav
|
|
const eyeIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`;
|
|
const listIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`;
|
|
const teamIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`;
|
|
const docIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`;
|
|
const quoteIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`;
|
|
const histIcon = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.5"/></svg>`;
|
|
|
|
// ── Helpers ──
|
|
const getStatusLabel = s => ({ demande:'Demande', planifie:'Planifié', en_cours:'En cours', termine:'Terminé', annule:'Annulé' }[s] || s);
|
|
const getStatusColor = s => ({ demande:'warning', planifie:'info', en_cours:'primary', termine:'success', annule:'danger' }[s] || 'secondary');
|
|
const getTypeLabel = t => ({ thanatopraxie:'Thanatopraxie', toilette_mortuaire:'Toilette mortuaire', exhumation:'Exhumation', retrait_pacemaker:'Retrait pacemaker', retrait_bijoux:'Retrait bijoux', autre:'Autre' }[t] || t);
|
|
const getQuoteStatusLabel = s => ({ brouillon:'Brouillon', envoye:'Envoyé', accepte:'Accepté', refuse:'Refusé', expire:'Expiré' }[s] || s || 'Inconnu');
|
|
const getQuoteStatusColor = s => ({ brouillon:'secondary', envoye:'info', accepte:'success', refuse:'danger', expire:'warning' }[s] || 'secondary');
|
|
const formatCurrency = v => new Intl.NumberFormat('fr-FR', { style:'currency', currency:'EUR', minimumFractionDigits:2 }).format(Number(v || 0));
|
|
const getInitials = name => name ? name.split(' ').map(w => w[0]).join('').toUpperCase().substring(0, 2) : '?';
|
|
|
|
// ── Document handlers ──
|
|
const handleFilesSelected = files => console.log('Files selected:', files.length);
|
|
const loadDocumentAttachments = async () => {
|
|
if (!props.intervention?.id) return;
|
|
try { await documentStore.fetchInterventionFiles(props.intervention.id); }
|
|
catch (e) { documentStore.clearError(); }
|
|
};
|
|
const handleUploadFiles = async files => {
|
|
if (!props.intervention?.id || !files.length) return;
|
|
try { await documentStore.uploadAndAttachFiles(files, 'App\\Models\\Intervention', props.intervention.id); }
|
|
catch (e) { documentStore.clearError(); }
|
|
};
|
|
const handleDeleteDocument = async id => {
|
|
try { await documentStore.detachFile(id); } catch (e) { documentStore.clearError(); }
|
|
};
|
|
const handleDeleteDocuments = async ids => {
|
|
try { await documentStore.detachMultipleFiles({ attachment_ids: ids }); } catch (e) { documentStore.clearError(); }
|
|
};
|
|
const handleUpdateDocumentLabel = async ({ id, label }) => {
|
|
try { await documentStore.updateAttachmentMetadata(id, { label }); } catch (e) { documentStore.clearError(); }
|
|
};
|
|
|
|
watch(() => props.activeTab, tab => {
|
|
if (tab === 'documents' && props.intervention?.id) loadDocumentAttachments();
|
|
});
|
|
watch(() => props.intervention?.id, id => {
|
|
if (id && props.activeTab === 'documents') loadDocumentAttachments();
|
|
});
|
|
onMounted(() => {
|
|
if (props.activeTab === 'documents' && props.intervention?.id) loadDocumentAttachments();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* ─── Design System ─────────────────────────────────────────────── */
|
|
:root {
|
|
--brand: #4f46e5;
|
|
--brand-light: #eef2ff;
|
|
--brand-dark: #3730a3;
|
|
--surface: #ffffff;
|
|
--surface-2: #f8fafc;
|
|
--surface-3: #f1f5f9;
|
|
--border: #e2e8f0;
|
|
--border-light: #f1f5f9;
|
|
--text-primary: #0f172a;
|
|
--text-secondary: #64748b;
|
|
--text-muted: #94a3b8;
|
|
--radius-sm: 8px;
|
|
--radius-md: 12px;
|
|
--radius-lg: 16px;
|
|
--shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
|
--shadow-md: 0 4px 16px rgba(0,0,0,.08);
|
|
--shadow-lg: 0 8px 32px rgba(0,0,0,.1);
|
|
}
|
|
|
|
/* ─── Page Shell ────────────────────────────────────────────────── */
|
|
.intervention-page {
|
|
min-height: 100vh;
|
|
background: var(--surface-2);
|
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
color: var(--text-primary);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* ─── Top Bar ───────────────────────────────────────────────────── */
|
|
.page-topbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 14px 28px;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 50;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.back-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
padding: 6px 12px;
|
|
border-radius: var(--radius-sm);
|
|
transition: background .15s, color .15s;
|
|
}
|
|
.back-btn:hover { background: var(--surface-3); color: var(--text-primary); }
|
|
|
|
.topbar-center {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin: 0 auto;
|
|
}
|
|
.topbar-id {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
letter-spacing: .3px;
|
|
}
|
|
.topbar-actions { display: flex; gap: 8px; }
|
|
|
|
/* ─── Layout ────────────────────────────────────────────────────── */
|
|
.page-layout {
|
|
display: grid;
|
|
grid-template-columns: 280px 1fr;
|
|
gap: 0;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
/* ─── Sidebar ───────────────────────────────────────────────────── */
|
|
.sidebar {
|
|
background: var(--surface);
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
position: sticky;
|
|
top: 57px;
|
|
height: calc(100vh - 57px);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.hero-card {
|
|
padding: 24px 20px 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
text-align: center;
|
|
border-bottom: 1px solid var(--border-light);
|
|
}
|
|
|
|
.hero-avatar {
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
margin-bottom: 4px;
|
|
box-shadow: 0 4px 16px rgba(79,70,229,.3);
|
|
}
|
|
|
|
.hero-name {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
line-height: 1.3;
|
|
}
|
|
.hero-type {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin: 0;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Quick Stats */
|
|
.quick-stats {
|
|
padding: 16px 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
border-bottom: 1px solid var(--border-light);
|
|
}
|
|
.stat-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
}
|
|
.stat-icon {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.date-icon { background: #eef2ff; color: #4f46e5; }
|
|
.loc-icon { background: #ecfdf5; color: #059669; }
|
|
.dur-icon { background: #fff7ed; color: #d97706; }
|
|
|
|
.stat-label { font-size: 11px; color: var(--text-muted); font-weight: 500; text-transform: uppercase; letter-spacing: .4px; }
|
|
.stat-value { font-size: 13px; color: var(--text-primary); font-weight: 500; margin-top: 1px; }
|
|
|
|
/* Sidebar Nav */
|
|
.sidebar-nav {
|
|
padding: 12px 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
flex: 1;
|
|
}
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 9px 12px;
|
|
border-radius: var(--radius-sm);
|
|
border: none;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
width: 100%;
|
|
text-align: left;
|
|
font-size: 13.5px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
transition: all .15s;
|
|
}
|
|
.nav-item:hover { background: var(--surface-3); color: var(--text-primary); }
|
|
.nav-item.active { background: var(--brand-light); color: var(--brand); font-weight: 600; }
|
|
.nav-item.active .nav-icon { color: var(--brand); }
|
|
.nav-icon { flex-shrink: 0; display: flex; color: var(--text-muted); transition: color .15s; }
|
|
.nav-label { flex: 1; }
|
|
.nav-badge {
|
|
min-width: 20px;
|
|
height: 20px;
|
|
padding: 0 6px;
|
|
background: var(--brand);
|
|
color: white;
|
|
border-radius: 10px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.nav-item.active .nav-badge { background: var(--brand-dark); }
|
|
|
|
/* Assign Button */
|
|
.assign-btn {
|
|
margin: 0 12px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 7px;
|
|
padding: 10px;
|
|
border: 1.5px dashed var(--border);
|
|
border-radius: var(--radius-sm);
|
|
background: transparent;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
transition: all .15s;
|
|
}
|
|
.assign-btn:hover { border-color: var(--brand); color: var(--brand); background: var(--brand-light); }
|
|
|
|
/* ─── Main Content ──────────────────────────────────────────────── */
|
|
.main-content {
|
|
padding: 28px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.tab-content {
|
|
animation: fadeIn .2s ease;
|
|
}
|
|
@keyframes fadeIn { from { opacity:0; transform: translateY(4px); } to { opacity:1; transform:translateY(0); } }
|
|
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 20px;
|
|
}
|
|
.section-header h3 {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
}
|
|
|
|
/* ─── Info Grid ─────────────────────────────────────────────────── */
|
|
.info-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
}
|
|
.info-grid .full-width { grid-column: 1 / -1; }
|
|
|
|
/* Info Section Card */
|
|
.info-section {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-md);
|
|
border: 1px solid var(--border);
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
.info-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 14px 18px;
|
|
border-bottom: 1px solid var(--border-light);
|
|
background: var(--surface-2);
|
|
}
|
|
.info-section-icon {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 7px;
|
|
background: var(--accent, #6366f1);
|
|
opacity: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
background: color-mix(in srgb, var(--accent, #6366f1) 15%, transparent);
|
|
color: var(--accent, #6366f1);
|
|
}
|
|
.info-section-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
text-transform: uppercase;
|
|
letter-spacing: .5px;
|
|
}
|
|
.info-section-body {
|
|
padding: 6px 18px 14px;
|
|
}
|
|
|
|
/* Data Row */
|
|
.data-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
padding: 9px 0;
|
|
border-bottom: 1px solid var(--border-light);
|
|
gap: 12px;
|
|
}
|
|
.data-row:last-child { border-bottom: none; }
|
|
.data-label {
|
|
font-size: 12.5px;
|
|
color: var(--text-muted);
|
|
font-weight: 500;
|
|
flex-shrink: 0;
|
|
}
|
|
.data-value {
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
text-align: right;
|
|
font-weight: 400;
|
|
}
|
|
.fw-semibold { font-weight: 600; }
|
|
|
|
.row-2col {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 0 24px;
|
|
}
|
|
|
|
.description-text {
|
|
font-size: 13.5px;
|
|
color: var(--text-secondary);
|
|
line-height: 1.7;
|
|
margin: 4px 0 0;
|
|
}
|
|
|
|
/* ─── Status Pills ──────────────────────────────────────────────── */
|
|
.status-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 3px 10px;
|
|
border-radius: 20px;
|
|
font-size: 11.5px;
|
|
font-weight: 600;
|
|
letter-spacing: .2px;
|
|
}
|
|
.status-pill.sp-lg { padding: 5px 14px; font-size: 12.5px; }
|
|
.sp-success { background: #dcfce7; color: #16a34a; }
|
|
.sp-warning { background: #fef9c3; color: #ca8a04; }
|
|
.sp-danger { background: #fee2e2; color: #dc2626; }
|
|
.sp-info { background: #dbeafe; color: #2563eb; }
|
|
.sp-primary { background: #eef2ff; color: #4f46e5; }
|
|
.sp-secondary { background: #f1f5f9; color: #64748b; }
|
|
|
|
/* ─── Buttons ───────────────────────────────────────────────────── */
|
|
.action-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 16px;
|
|
border-radius: var(--radius-sm);
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
transition: all .15s;
|
|
}
|
|
.action-btn.primary {
|
|
background: var(--brand);
|
|
color: white;
|
|
}
|
|
.action-btn.primary:hover { background: var(--brand-dark); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(79,70,229,.3); }
|
|
.action-btn.secondary {
|
|
background: var(--surface-3);
|
|
color: var(--text-secondary);
|
|
}
|
|
.action-btn.secondary:hover { background: var(--border); color: var(--text-primary); }
|
|
.action-btn.sm { padding: 6px 12px; font-size: 12px; }
|
|
|
|
/* ─── Team Grid ─────────────────────────────────────────────────── */
|
|
.team-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
.practitioner-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
padding: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
box-shadow: var(--shadow-sm);
|
|
transition: box-shadow .15s;
|
|
}
|
|
.practitioner-card:hover { box-shadow: var(--shadow-md); }
|
|
.pract-avatar {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
}
|
|
.pract-info { flex: 1; }
|
|
.pract-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
|
|
.pract-role { margin-top: 3px; }
|
|
.role-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
.role-principal { background: #eef2ff; color: #4f46e5; }
|
|
.role-assistant { background: #f0fdf4; color: #16a34a; }
|
|
.unassign-btn {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-muted);
|
|
transition: all .15s;
|
|
}
|
|
.unassign-btn:hover { background: #fee2e2; border-color: #fca5a5; color: #dc2626; }
|
|
|
|
/* ─── Quote ─────────────────────────────────────────────────────── */
|
|
.quote-status-badge {
|
|
display: inline-block;
|
|
padding: 2px 10px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
.qs-success { background: #dcfce7; color: #16a34a; }
|
|
.qs-info { background: #dbeafe; color: #2563eb; }
|
|
.qs-warning { background: #fef9c3; color: #ca8a04; }
|
|
.qs-danger { background: #fee2e2; color: #dc2626; }
|
|
.qs-secondary { background: #f1f5f9; color: #64748b; }
|
|
|
|
.quote-lines-section { margin-top: 20px; }
|
|
.quote-table-header {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: .5px;
|
|
}
|
|
.quote-table-wrapper {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
.quote-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13px;
|
|
}
|
|
.quote-table thead { background: var(--surface-2); }
|
|
.quote-table th {
|
|
padding: 11px 16px;
|
|
font-size: 11.5px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: .4px;
|
|
border-bottom: 1px solid var(--border);
|
|
text-align: left;
|
|
}
|
|
.quote-table td {
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--border-light);
|
|
color: var(--text-primary);
|
|
}
|
|
.quote-table tbody tr:last-child td { border-bottom: none; }
|
|
.quote-table tbody tr:hover td { background: var(--surface-2); }
|
|
.text-center { text-align: center; }
|
|
.text-right { text-align: right; }
|
|
|
|
/* ─── Empty / Loading States ────────────────────────────────────── */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 64px 24px;
|
|
text-align: center;
|
|
gap: 12px;
|
|
}
|
|
.empty-icon {
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 50%;
|
|
background: var(--surface-3);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-muted);
|
|
margin-bottom: 4px;
|
|
}
|
|
.empty-state p { font-size: 14px; color: var(--text-secondary); margin: 0; font-weight: 500; }
|
|
.coming-soon {
|
|
font-size: 11.5px;
|
|
color: var(--text-muted);
|
|
background: var(--surface-3);
|
|
padding: 3px 10px;
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.fullpage-loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 1;
|
|
gap: 16px;
|
|
padding: 80px;
|
|
}
|
|
.loading-orb {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
border: 3px solid var(--border);
|
|
border-top-color: var(--brand);
|
|
animation: spin .8s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.fullpage-loading p { font-size: 14px; color: var(--text-secondary); }
|
|
|
|
.fullpage-error {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 1;
|
|
gap: 12px;
|
|
padding: 80px;
|
|
text-align: center;
|
|
}
|
|
.error-icon {
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 50%;
|
|
background: #fee2e2;
|
|
color: #dc2626;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.fullpage-error h3 { font-size: 18px; font-weight: 700; color: var(--text-primary); margin: 0; }
|
|
.fullpage-error p { font-size: 14px; color: var(--text-secondary); margin: 0; }
|
|
|
|
/* ─── Responsive ────────────────────────────────────────────────── */
|
|
@media (max-width: 900px) {
|
|
.page-layout { grid-template-columns: 1fr; }
|
|
.sidebar {
|
|
position: static;
|
|
height: auto;
|
|
border-right: none;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.sidebar-nav { flex-direction: row; flex-wrap: wrap; padding: 8px; }
|
|
.nav-item { flex: 0 1 auto; }
|
|
.info-grid { grid-template-columns: 1fr; }
|
|
.info-grid .full-width { grid-column: 1; }
|
|
.main-content { padding: 16px; }
|
|
}
|
|
</style>
|