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.
231 lines
10 KiB
Vue
231 lines
10 KiB
Vue
<template>
|
|
<div class="sidebar-wrap">
|
|
<!-- Hero Card -->
|
|
<div class="hero-card">
|
|
<div class="hero-avatar">
|
|
<svg width="26" height="26" 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>
|
|
<h2 class="hero-name">{{ intervention.defuntName || 'Personne inconnue' }}</h2>
|
|
<p class="hero-type">{{ intervention.title || 'Type non défini' }}</p>
|
|
<div class="status-badge" :class="'sb-' + (intervention.status?.color || 'secondary')">
|
|
{{ intervention.status?.label || 'En attente' }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="quick-stats">
|
|
<div class="qs-row">
|
|
<div class="qs-icon" style="background:#eef2ff;color:#4f46e5">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="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 class="qs-text">
|
|
<div class="qs-label">Date</div>
|
|
<div class="qs-value">{{ intervention.date || '—' }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="qs-row">
|
|
<div class="qs-icon" style="background:#ecfdf5;color:#059669">
|
|
<svg width="13" height="13" 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 class="qs-text">
|
|
<div class="qs-label">Lieu</div>
|
|
<div class="qs-value">{{ intervention.lieux || '—' }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="qs-row">
|
|
<div class="qs-icon" style="background:#fff7ed;color:#d97706">
|
|
<svg width="13" height="13" 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 class="qs-text">
|
|
<div class="qs-label">Durée</div>
|
|
<div class="qs-value">{{ intervention.duree || '—' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
|
|
<!-- Team preview -->
|
|
<div v-if="intervention.members?.length" class="team-preview">
|
|
<div class="tp-label">Équipe</div>
|
|
<div class="tp-avatars">
|
|
<div
|
|
v-for="(m, i) in intervention.members.slice(0, 5)"
|
|
:key="i"
|
|
class="tp-avatar"
|
|
:title="m.name"
|
|
:style="{ zIndex: 10 - i }"
|
|
>
|
|
{{ getInitials(m.name) }}
|
|
</div>
|
|
<div v-if="intervention.members.length > 5" class="tp-avatar tp-more">
|
|
+{{ intervention.members.length - 5 }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
|
|
<!-- Tab Navigation -->
|
|
<nav class="tab-nav">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.id"
|
|
class="tab-item"
|
|
:class="{ active: activeTab === tab.id }"
|
|
@click="$emit('change-tab', tab.id)"
|
|
>
|
|
<span class="tab-icon" v-html="tab.icon"></span>
|
|
<span class="tab-label">{{ tab.label }}</span>
|
|
<span v-if="tab.id === 'team' && teamCount > 0" class="tab-badge">{{ teamCount }}</span>
|
|
<span v-if="tab.id === 'documents' && documentsCount > 0" class="tab-badge">{{ documentsCount }}</span>
|
|
</button>
|
|
</nav>
|
|
|
|
<!-- Assign Button -->
|
|
<div class="assign-wrap">
|
|
<button class="assign-btn" @click="$emit('assign-practitioner')">
|
|
<svg width="13" height="13" 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>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { defineProps, defineEmits } from 'vue';
|
|
|
|
defineProps({
|
|
intervention: { type: Object, required: true },
|
|
activeTab: { type: String, default: 'overview' },
|
|
practitioners:{ type: Array, default: () => [] },
|
|
teamCount: { type: Number, default: 0 },
|
|
documentsCount:{ type: Number, default: 0 },
|
|
});
|
|
defineEmits(['change-tab', 'assign-practitioner']);
|
|
|
|
const getInitials = n => n ? n.split(' ').map(w => w[0]).join('').toUpperCase().substring(0,2) : '?';
|
|
|
|
const tabs = [
|
|
{ id:'overview', label:"Vue d'ensemble", icon:`<svg width="14" height="14" 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>` },
|
|
{ id:'details', label:'Détails', icon:`<svg width="14" height="14" 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>` },
|
|
{ id:'team', label:'Équipe', icon:`<svg width="14" height="14" 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>` },
|
|
{ id:'documents', label:'Documents', icon:`<svg width="14" height="14" 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>` },
|
|
{ id:'quote', label:'Devis', icon:`<svg width="14" height="14" 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"/></svg>` },
|
|
{ id:'history', label:'Historique', icon:`<svg width="14" height="14" 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>` },
|
|
];
|
|
</script>
|
|
|
|
<style scoped>
|
|
.sidebar-wrap {
|
|
--brand: #4f46e5;
|
|
--brand-lt: #eef2ff;
|
|
--brand-dk: #3730a3;
|
|
--surface: #ffffff;
|
|
--surface-2:#f8fafc;
|
|
--border: #e2e8f0;
|
|
--border-lt:#f1f5f9;
|
|
--text-1: #0f172a;
|
|
--text-2: #64748b;
|
|
--text-3: #94a3b8;
|
|
--r-sm: 8px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
overflow: hidden;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
|
font-family: 'Inter', system-ui, sans-serif;
|
|
}
|
|
|
|
/* Hero */
|
|
.hero-card {
|
|
padding: 24px 20px 18px;
|
|
display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center;
|
|
}
|
|
.hero-avatar {
|
|
width: 58px; height: 58px; border-radius: 50%;
|
|
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
|
display: flex; align-items: center; justify-content: center;
|
|
color: white; margin-bottom: 2px;
|
|
box-shadow: 0 4px 14px rgba(79,70,229,.28);
|
|
}
|
|
.hero-name { font-size: 15px; font-weight: 700; color: var(--text-1); margin: 0; line-height: 1.3; }
|
|
.hero-type { font-size: 12px; color: var(--text-2); margin: 0; font-weight: 500; }
|
|
|
|
/* Status badge */
|
|
.status-badge { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 20px; font-size: 11.5px; font-weight: 600; }
|
|
.sb-success { background:#dcfce7; color:#16a34a; }
|
|
.sb-warning { background:#fef9c3; color:#ca8a04; }
|
|
.sb-danger { background:#fee2e2; color:#dc2626; }
|
|
.sb-info { background:#dbeafe; color:#2563eb; }
|
|
.sb-primary { background:#eef2ff; color:#4f46e5; }
|
|
.sb-secondary{ background:#f1f5f9; color:#64748b; }
|
|
|
|
.divider { height: 1px; background: var(--border-lt); }
|
|
|
|
/* Quick stats */
|
|
.quick-stats { padding: 14px 18px; display: flex; flex-direction: column; gap: 10px; }
|
|
.qs-row { display: flex; align-items: flex-start; gap: 10px; }
|
|
.qs-icon { width: 28px; height: 28px; border-radius: 7px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
.qs-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--text-3); font-weight: 600; }
|
|
.qs-value { font-size: 12.5px; color: var(--text-1); font-weight: 500; margin-top: 1px; }
|
|
|
|
/* Team preview */
|
|
.team-preview { padding: 12px 18px; display: flex; align-items: center; gap: 12px; }
|
|
.tp-label { font-size: 11.5px; font-weight: 600; color: var(--text-3); text-transform: uppercase; letter-spacing: .4px; }
|
|
.tp-avatars { display: flex; }
|
|
.tp-avatar {
|
|
width: 30px; height: 30px; border-radius: 50%;
|
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
|
border: 2px solid var(--surface);
|
|
color: white; font-size: 10px; font-weight: 700;
|
|
display: flex; align-items: center; justify-content: center;
|
|
margin-left: -6px; cursor: default;
|
|
transition: transform .15s;
|
|
}
|
|
.tp-avatar:first-child { margin-left: 0; }
|
|
.tp-avatar:hover { transform: translateY(-3px); }
|
|
.tp-more { background: var(--surface-2); color: var(--text-2); font-size: 9px; }
|
|
|
|
/* Tab nav */
|
|
.tab-nav { padding: 8px 10px; display: flex; flex-direction: column; gap: 2px; }
|
|
.tab-item {
|
|
display: flex; align-items: center; gap: 9px; padding: 8px 11px;
|
|
border-radius: var(--r-sm); border: none; background: transparent; cursor: pointer;
|
|
width: 100%; text-align: left; font-size: 13px; font-weight: 500; color: var(--text-2);
|
|
transition: all .12s;
|
|
}
|
|
.tab-item:hover { background: var(--surface-2); color: var(--text-1); }
|
|
.tab-item.active { background: var(--brand-lt); color: var(--brand); font-weight: 600; }
|
|
.tab-icon { flex-shrink: 0; display: flex; color: var(--text-3); }
|
|
.tab-item.active .tab-icon { color: var(--brand); }
|
|
.tab-label { flex: 1; }
|
|
.tab-badge {
|
|
min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
|
|
background: var(--brand); color: white; font-size: 10px; font-weight: 700;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
|
|
/* Assign */
|
|
.assign-wrap { padding: 0 10px 12px; }
|
|
.assign-btn {
|
|
width: 100%; display: flex; align-items: center; justify-content: center; gap: 7px;
|
|
padding: 9px; border: 1.5px dashed var(--border); border-radius: var(--r-sm);
|
|
background: transparent; cursor: pointer; font-size: 12.5px; font-weight: 500; color: var(--text-2);
|
|
transition: all .15s;
|
|
}
|
|
.assign-btn:hover { border-color: var(--brand); color: var(--brand); background: var(--brand-lt); }
|
|
</style>
|