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.
269 lines
10 KiB
Vue
269 lines
10 KiB
Vue
<template>
|
|
<!-- Backdrop -->
|
|
<Teleport to="body">
|
|
<Transition name="modal-fade">
|
|
<div v-if="isOpen" class="modal-backdrop" @mousedown.self="$emit('close')">
|
|
<div class="modal-box" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
|
|
<!-- Header -->
|
|
<div class="modal-header">
|
|
<div class="modal-title-wrap">
|
|
<div class="modal-icon">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<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>
|
|
</div>
|
|
<div>
|
|
<h2 id="modal-title" class="modal-title">Assigner un praticien</h2>
|
|
<p class="modal-sub">Sélectionnez un praticien et son rôle</p>
|
|
</div>
|
|
</div>
|
|
<button class="close-btn" @click="$emit('close')">
|
|
<svg width="16" height="16" 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>
|
|
|
|
<!-- Body -->
|
|
<div class="modal-body">
|
|
<!-- Practitioner ID -->
|
|
<div class="form-group">
|
|
<label class="form-label">Identifiant du praticien</label>
|
|
<input
|
|
v-model="form.practitionerId"
|
|
type="number"
|
|
class="form-input"
|
|
placeholder="ex: 42"
|
|
min="1"
|
|
/>
|
|
<p class="form-hint">Entrez l'ID du praticien à assigner à cette intervention.</p>
|
|
</div>
|
|
|
|
<!-- Role -->
|
|
<div class="form-group">
|
|
<label class="form-label">Rôle</label>
|
|
<div class="role-grid">
|
|
<button
|
|
class="role-option"
|
|
:class="{ selected: form.role === 'principal' }"
|
|
@click="form.role = 'principal'"
|
|
type="button"
|
|
>
|
|
<div class="role-radio">
|
|
<div v-if="form.role === 'principal'" class="role-radio-dot"></div>
|
|
</div>
|
|
<div class="role-info">
|
|
<div class="role-name">Principal</div>
|
|
<div class="role-desc">Responsable de l'intervention</div>
|
|
</div>
|
|
<span class="role-chip chip-principal">Principal</span>
|
|
</button>
|
|
|
|
<button
|
|
class="role-option"
|
|
:class="{ selected: form.role === 'assistant' }"
|
|
@click="form.role = 'assistant'"
|
|
type="button"
|
|
>
|
|
<div class="role-radio">
|
|
<div v-if="form.role === 'assistant'" class="role-radio-dot"></div>
|
|
</div>
|
|
<div class="role-info">
|
|
<div class="role-name">Assistant</div>
|
|
<div class="role-desc">Rôle de soutien et assistance</div>
|
|
</div>
|
|
<span class="role-chip chip-assistant">Assistant</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Validation error -->
|
|
<Transition name="slide-error">
|
|
<div v-if="error" class="error-banner">
|
|
<svg width="14" height="14" 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>
|
|
{{ error }}
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="modal-footer">
|
|
<button class="btn-ghost" @click="$emit('close')">Annuler</button>
|
|
<button class="btn-primary" @click="handleSubmit">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
|
|
Confirmer l'assignation
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, defineProps, defineEmits } from 'vue';
|
|
|
|
const props = defineProps({
|
|
isOpen: { type: Boolean, default: false },
|
|
});
|
|
const emit = defineEmits(['close', 'assign']);
|
|
|
|
const form = ref({ practitionerId: '', role: 'principal' });
|
|
const error = ref('');
|
|
|
|
// Reset form when modal opens
|
|
watch(() => props.isOpen, open => {
|
|
if (open) { form.value = { practitionerId: '', role: 'principal' }; error.value = ''; }
|
|
});
|
|
|
|
const handleSubmit = () => {
|
|
error.value = '';
|
|
if (!form.value.practitionerId) {
|
|
error.value = 'Veuillez entrer un identifiant de praticien.';
|
|
return;
|
|
}
|
|
if (parseInt(form.value.practitionerId) <= 0) {
|
|
error.value = 'L\'identifiant doit être un nombre positif.';
|
|
return;
|
|
}
|
|
emit('assign', {
|
|
practitionerId: parseInt(form.value.practitionerId),
|
|
role: form.value.role,
|
|
});
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* ── Tokens ── */
|
|
.modal-backdrop {
|
|
--brand: #4f46e5;
|
|
--brand-lt: #eef2ff;
|
|
--brand-dk: #3730a3;
|
|
--surface: #ffffff;
|
|
--surface-2:#f8fafc;
|
|
--border: #e2e8f0;
|
|
--text-1: #0f172a;
|
|
--text-2: #64748b;
|
|
--text-3: #94a3b8;
|
|
--r-sm: 8px;
|
|
--r-md: 12px;
|
|
position: fixed; inset: 0;
|
|
background: rgba(15,23,42,.45);
|
|
backdrop-filter: blur(4px);
|
|
display: flex; align-items: center; justify-content: center;
|
|
z-index: 1000; padding: 20px;
|
|
font-family: 'Inter', system-ui, sans-serif;
|
|
}
|
|
|
|
/* ── Modal box ── */
|
|
.modal-box {
|
|
background: var(--surface);
|
|
border-radius: 16px;
|
|
width: 100%; max-width: 460px;
|
|
box-shadow: 0 24px 64px rgba(0,0,0,.18), 0 0 0 1px rgba(0,0,0,.05);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Header */
|
|
.modal-header {
|
|
display: flex; align-items: flex-start; justify-content: space-between;
|
|
padding: 22px 24px 18px; border-bottom: 1px solid var(--border);
|
|
}
|
|
.modal-title-wrap { display: flex; align-items: center; gap: 12px; }
|
|
.modal-icon {
|
|
width: 38px; height: 38px; border-radius: 10px;
|
|
background: var(--brand-lt); color: var(--brand);
|
|
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
|
}
|
|
.modal-title { font-size: 16px; font-weight: 700; color: var(--text-1); margin: 0; }
|
|
.modal-sub { font-size: 12.5px; color: var(--text-3); margin: 2px 0 0; }
|
|
|
|
.close-btn {
|
|
width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--border);
|
|
background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
color: var(--text-3); transition: all .15s; flex-shrink: 0;
|
|
}
|
|
.close-btn:hover { background: var(--surface-2); color: var(--text-1); }
|
|
|
|
/* Body */
|
|
.modal-body { padding: 20px 24px; display: flex; flex-direction: column; gap: 18px; }
|
|
|
|
.form-group { display: flex; flex-direction: column; gap: 6px; }
|
|
.form-label { font-size: 12px; font-weight: 700; color: var(--text-2); text-transform: uppercase; letter-spacing: .5px; }
|
|
.form-hint { font-size: 11.5px; color: var(--text-3); margin: 2px 0 0; }
|
|
|
|
.form-input {
|
|
padding: 9px 12px; border: 1px solid var(--border); border-radius: var(--r-sm);
|
|
font-size: 13.5px; color: var(--text-1); background: var(--surface); outline: none;
|
|
transition: border-color .15s, box-shadow .15s; font-family: inherit; width: 100%;
|
|
}
|
|
.form-input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(79,70,229,.1); }
|
|
|
|
/* Role grid */
|
|
.role-grid { display: flex; flex-direction: column; gap: 8px; }
|
|
.role-option {
|
|
display: flex; align-items: center; gap: 12px; padding: 12px 14px;
|
|
border: 1.5px solid var(--border); border-radius: var(--r-sm);
|
|
background: transparent; cursor: pointer; text-align: left; width: 100%;
|
|
transition: all .15s;
|
|
}
|
|
.role-option:hover { border-color: #a5b4fc; background: var(--brand-lt); }
|
|
.role-option.selected { border-color: var(--brand); background: var(--brand-lt); }
|
|
|
|
.role-radio {
|
|
width: 17px; height: 17px; border-radius: 50%; border: 2px solid var(--border);
|
|
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
|
transition: border-color .15s;
|
|
}
|
|
.role-option.selected .role-radio { border-color: var(--brand); }
|
|
.role-radio-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--brand); }
|
|
|
|
.role-info { flex: 1; }
|
|
.role-name { font-size: 13.5px; font-weight: 600; color: var(--text-1); }
|
|
.role-desc { font-size: 11.5px; color: var(--text-3); margin-top: 1px; }
|
|
|
|
.role-chip { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10.5px; font-weight: 600; }
|
|
.chip-principal { background: #eef2ff; color: #4f46e5; }
|
|
.chip-assistant { background: #f0fdf4; color: #16a34a; }
|
|
|
|
/* Error */
|
|
.error-banner {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 10px 13px; background: #fef2f2; border: 1px solid #fecaca;
|
|
border-radius: var(--r-sm); font-size: 13px; color: #dc2626; font-weight: 500;
|
|
}
|
|
|
|
/* Footer */
|
|
.modal-footer {
|
|
display: flex; gap: 10px; justify-content: flex-end;
|
|
padding: 16px 24px; background: var(--surface-2); border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.btn-primary {
|
|
display: inline-flex; align-items: center; gap: 7px;
|
|
padding: 9px 18px; border-radius: var(--r-sm); border: none; cursor: pointer;
|
|
font-size: 13px; font-weight: 600; color: white; background: var(--brand);
|
|
transition: all .15s;
|
|
}
|
|
.btn-primary:hover { background: var(--brand-dk); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(79,70,229,.3); }
|
|
|
|
.btn-ghost {
|
|
display: inline-flex; align-items: center; gap: 7px;
|
|
padding: 9px 16px; border-radius: var(--r-sm); border: 1px solid var(--border);
|
|
cursor: pointer; font-size: 13px; font-weight: 500; color: var(--text-2); background: transparent;
|
|
transition: all .15s;
|
|
}
|
|
.btn-ghost:hover { background: var(--border); color: var(--text-1); }
|
|
|
|
/* ── Transitions ── */
|
|
.modal-fade-enter-active, .modal-fade-leave-active { transition: opacity .2s ease; }
|
|
.modal-fade-enter-active .modal-box, .modal-fade-leave-active .modal-box { transition: transform .2s ease, opacity .2s ease; }
|
|
.modal-fade-enter-from, .modal-fade-leave-to { opacity: 0; }
|
|
.modal-fade-enter-from .modal-box, .modal-fade-leave-to .modal-box { transform: scale(.96) translateY(8px); opacity: 0; }
|
|
|
|
.slide-error-enter-active, .slide-error-leave-active { transition: all .2s ease; }
|
|
.slide-error-enter-from, .slide-error-leave-to { opacity: 0; transform: translateY(-6px); }
|
|
</style>
|