New-Thanasoft/thanasoft-front/refont_interview/AssignPractitionerModal.vue
nyavokevin 9cbc1bcbdb feat(ui): add price lists and group-based quote flows
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.
2026-04-02 12:07:11 +03:00

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>