{% extends 'basecaisse.html.twig' %}
{% block title %}Point de Vente{% endblock %}
{% block stylesheets %}{% endblock %}
{% block body %}
<style>
:root {
--pos-primary: #1a56db;
--pos-primary-dark: #1341b0;
--pos-success: #059669;
--pos-success-light: #d1fae5;
--pos-danger: #dc2626;
--pos-warning: #d97706;
--pos-warning-light: #fffbeb;
--pos-warning-border: #fcd34d;
--pos-gray-50: #f8fafc;
--pos-gray-100: #f1f5f9;
--pos-gray-200: #e2e8f0;
--pos-gray-400: #94a3b8;
--pos-gray-600: #475569;
--pos-gray-800: #1e293b;
--pos-radius: 10px;
--pos-shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.05);
--pos-shadow-md: 0 4px 12px rgba(0,0,0,.1);
--pos-header-grad: linear-gradient(120deg, #0b1230 0%, #101d4a 30%, #142a70 55%, #1a3fae 80%, #1a56db 100%);
--pos-header-border: rgba(96,165,250,.45);
--pos-header-h: 68px;
}
/* ===================== LAYOUT ===================== */
.pos-wrapper {
display: grid;
grid-template-columns: 1fr clamp(420px, 33vw, 560px);
grid-template-rows: auto 1fr;
height: 100vh;
overflow: hidden;
background: var(--pos-gray-100);
gap: 0;
}
.pos-header {
grid-column: 1 / -1;
background: var(--pos-header-grad);
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22px;
height: var(--pos-header-h);
border-bottom: 2px solid var(--pos-header-border);
box-shadow: 0 6px 18px rgba(2,6,23,.35);
z-index: 10;
flex-wrap: nowrap;
gap: 10px;
}
.pos-products {
overflow-y: auto;
display: flex;
flex-direction: column;
background: var(--pos-gray-100);
min-width: 0;
}
.pos-cart {
background: #fff;
border-left: 1px solid var(--pos-gray-200);
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.cart-close-btn { display: none; }
/* Bouton flottant panier (mobile uniquement) */
.cart-fab {
display: none;
position: fixed;
right: 16px;
bottom: 16px;
z-index: 900;
background: var(--pos-success);
color: #fff;
border: none;
border-radius: 999px;
padding: 12px 18px;
box-shadow: 0 6px 18px rgba(0,0,0,.25);
font-size: 14px;
font-weight: 700;
align-items: center;
gap: 8px;
cursor: pointer;
}
.cart-fab .fab-count {
background: rgba(255,255,255,.25);
border-radius: 999px;
padding: 1px 8px;
font-size: 12px;
}
.cart-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(15,23,42,.45);
z-index: 899;
}
/* ===================== HEADER ===================== */
.header-client {
display: flex;
align-items: center;
gap: 10px;
}
.client-badge {
display: flex;
align-items: center;
gap: 10px;
min-height: 44px;
background: rgba(255,255,255,.12);
border: 1px solid rgba(255,255,255,.22);
border-radius: 26px;
padding: 6px 18px 6px 6px;
cursor: pointer;
transition: background .2s;
}
.client-badge:hover { background: rgba(255,255,255,.22); }
.client-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(255,255,255,.28);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 700;
color: #fff;
flex-shrink: 0;
}
.client-name {
font-size: 15px;
font-weight: 700;
color: #fff;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.header-right {
display: flex;
align-items: center;
gap: 10px;
}
.total-display {
background: rgba(255,255,255,.14);
border: 1px solid rgba(255,255,255,.24);
border-radius: 12px;
padding: 9px 20px;
font-size: 21px;
font-weight: 800;
color: #fff;
letter-spacing: .3px;
min-height: 44px;
display: flex;
align-items: center;
}
/* ===================== USER SESSION / SHIFT CHANGE ===================== */
.user-session-chip {
display: flex;
align-items: center;
gap: 10px;
min-height: 44px;
background: rgba(255,255,255,.12);
border: 1px solid rgba(255,255,255,.22);
border-radius: 26px;
padding: 5px 5px 5px 14px;
}
.user-session-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
background: rgba(255,255,255,.28);
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
color: #fff;
flex-shrink: 0;
}
.user-session-name {
font-size: 14px;
font-weight: 700;
color: #fff;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-session-logout {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 50%;
background: rgba(220,38,38,.9);
color: #fff;
font-size: 14px;
text-decoration: none;
transition: background .15s;
flex-shrink: 0;
}
.user-session-logout:hover { background: rgba(185,28,28,.95); color: #fff; }
/* ===================== BRAND / LOGO ===================== */
.pos-brand {
display: flex;
align-items: center;
padding-left: 16px;
margin-left: 4px;
border-left: 1px solid rgba(255,255,255,.2);
user-select: none;
flex-shrink: 0;
}
.pos-brand-text {
display: inline-flex;
align-items: baseline;
font-size: 19px;
font-weight: 800;
letter-spacing: -.3px;
white-space: nowrap;
background: linear-gradient(100deg, #ffffff 0%, #ffffff 60%, #bfdbfe 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 1px 12px rgba(96,165,250,.35);
}
.pos-brand-tld {
font-weight: 600;
color: #7dabfa;
-webkit-text-fill-color: #7dabfa;
}
@media (max-width: 640px) {
.user-session-name { display: none; }
.user-session-chip { padding: 5px; gap: 6px; }
.pos-brand { padding-left: 10px; }
.pos-brand-text { font-size: 16px; }
}
@media (max-width: 420px) {
.pos-brand { display: none; }
}
.btn-dashboard {
background: rgba(255,255,255,.14);
border: 1px solid rgba(255,255,255,.24);
border-radius: 12px;
padding: 10px 18px;
min-height: 44px;
font-size: 14px;
font-weight: 600;
color: #fff;
text-decoration: none;
transition: background .15s, transform .1s;
display: flex;
align-items: center;
gap: 9px;
}
.btn-dashboard:hover {
background: rgba(255,255,255,.26);
color: #fff;
text-decoration: none;
}
.btn-dashboard:active { transform: scale(.97); }
/* ===================== CATEGORIES ===================== */
.category-bar {
background: #fff;
border-bottom: 1px solid var(--pos-gray-200);
padding: 10px 16px;
display: flex;
gap: 8px;
overflow-x: auto;
flex-shrink: 0;
}
.category-bar::-webkit-scrollbar { height: 3px; }
.category-bar::-webkit-scrollbar-thumb { background: var(--pos-gray-200); border-radius: 3px; }
.cat-pill {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 10px 20px;
border-radius: 22px;
border: 1px solid var(--pos-gray-200);
background: var(--pos-gray-50);
font-size: 14.5px;
font-weight: 600;
color: var(--pos-gray-600);
text-decoration: none;
white-space: nowrap;
transition: all .15s;
flex-shrink: 0;
}
.cat-pill:active { transform: scale(.96); }
.cat-pill:hover {
background: var(--pos-gray-100);
border-color: var(--pos-gray-400);
color: var(--pos-gray-800);
text-decoration: none;
}
.cat-pill.active {
background: var(--pos-primary);
border-color: var(--pos-primary);
color: #fff;
}
/* ===================== SEARCH ===================== */
.search-bar {
padding: 10px 16px;
background: #fff;
border-bottom: 1px solid var(--pos-gray-200);
flex-shrink: 0;
}
.search-inner {
position: relative;
}
.search-inner i {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--pos-gray-400);
font-size: 13px;
}
.search-input {
width: 100%;
padding: 8px 12px 8px 36px;
border: 1px solid var(--pos-gray-200);
border-radius: 8px;
font-size: 13px;
background: var(--pos-gray-50);
color: var(--pos-gray-800);
outline: none;
transition: border-color .18s, box-shadow .18s;
}
.search-input:focus {
border-color: var(--pos-primary);
box-shadow: 0 0 0 3px rgba(26,86,219,.1);
background: #fff;
}
/* ===================== PRODUCT GRID ===================== */
.products-scroll {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(155px, 1fr));
gap: 12px;
}
.product-card {
background: #fff;
border-radius: var(--pos-radius);
overflow: hidden;
cursor: pointer;
border: 1px solid var(--pos-gray-200);
transition: transform .18s, box-shadow .18s, border-color .18s;
position: relative;
}
.product-card:hover {
transform: translateY(-3px);
box-shadow: var(--pos-shadow-md);
border-color: var(--pos-primary);
}
.product-card:active { transform: scale(.97); }
.product-img {
height: 98px;
background-size: cover;
background-position: center;
background-color: var(--pos-gray-100);
position: relative;
}
.product-body {
padding: 10px;
}
.product-name {
font-size: 12px;
font-weight: 600;
color: var(--pos-gray-800);
line-height: 1.35;
margin-bottom: 6px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-price {
font-size: 14px;
font-weight: 700;
color: var(--pos-primary);
}
.product-price-buy {
font-size: 10px;
color: var(--pos-gray-400);
margin-top: 2px;
}
.discount-badge {
position: absolute;
top: 8px;
right: 8px;
background: #ef4444;
color: #fff;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 10px;
}
.no-products {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
color: var(--pos-gray-400);
font-size: 14px;
}
/* ===================== CART ===================== */
.cart-header {
padding: 14px 16px;
background: var(--pos-primary);
color: #fff;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.cart-body {
flex: 1;
min-height: 120px;
overflow-y: auto;
padding: 10px;
background: var(--pos-gray-50);
}
.cart-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--pos-gray-400);
gap: 10px;
font-size: 13px;
}
.cart-item {
display: block; /* basecaisse.html.twig impose display:flex (ancien panier) */
background: #fff;
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 8px;
border: 1px solid var(--pos-gray-200);
border-left: 3px solid var(--pos-primary);
transition: border-color .18s;
}
.cart-item.has-discount { border-left-color: var(--pos-warning); }
.cart-item-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
}
.cart-item-name {
font-size: 12px;
font-weight: 600;
color: var(--pos-gray-800);
line-height: 1.3;
flex: 1;
min-width: 0;
}
.cart-item-total {
font-size: 13px;
font-weight: 700;
color: var(--pos-primary);
white-space: nowrap;
flex-shrink: 0;
}
.cart-item-meta {
font-size: 11px;
color: var(--pos-gray-400);
margin: 3px 0 8px;
}
.cart-item-controls {
display: flex;
align-items: center;
gap: 6px;
}
.qty-btn {
width: 38px;
height: 38px;
border-radius: 9px;
border: 1px solid var(--pos-gray-200);
background: var(--pos-gray-50);
color: var(--pos-gray-600);
font-size: 18px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all .15s;
padding: 0;
}
.qty-btn:hover { background: var(--pos-gray-200); }
.qty-btn:active { transform: scale(.94); }
.qty-display {
font-size: 16px;
font-weight: 700;
color: var(--pos-gray-800);
min-width: 28px;
text-align: center;
}
.btn-remove {
margin-left: auto;
width: 38px;
height: 38px;
border-radius: 9px;
border: 1px solid #fecaca;
background: #fef2f2;
color: var(--pos-danger);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all .15s;
padding: 0;
}
.btn-remove:hover { background: #fee2e2; }
.btn-remove:active { transform: scale(.94); }
.discount-row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
}
.discount-row i.fa-tag {
flex-shrink: 0;
}
.discount-input-small {
flex: 1 1 44px;
min-width: 36px;
width: auto;
height: 36px;
border: 1px solid var(--pos-gray-200);
border-radius: 8px;
font-size: 13px;
text-align: right;
padding: 0 8px;
color: var(--pos-gray-800);
background: var(--pos-gray-50);
outline: none;
}
.discount-input-small:focus {
border-color: var(--pos-warning);
background: #fff;
}
.discount-submit-btn {
flex-shrink: 0;
height: 36px;
padding: 0 14px;
border: 1px solid var(--pos-warning);
background: var(--pos-warning-light);
color: var(--pos-warning);
border-radius: 8px;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: all .15s;
}
.discount-submit-btn:hover { background: #fef3c7; }
/* ===================== CART SUMMARY ===================== */
.cart-footer {
flex-shrink: 0;
border-top: 1px solid var(--pos-gray-200);
background: #fff;
/* Les articles gardent toujours de la place, même avec les boutons restaurant */
max-height: 58%;
overflow-y: auto;
}
.cart-summary {
padding: 12px 16px 0;
}
.summary-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--pos-gray-600);
padding: 3px 0;
}
.summary-row.ttc-row {
font-size: 15px;
font-weight: 700;
color: var(--pos-gray-800);
background: #eff6ff;
border-radius: 8px;
padding: 8px 10px;
margin: 6px 0 0;
}
/* Remise globale */
.global-discount-block {
margin: 10px 0 4px;
background: #fffbeb;
border: 1.5px solid #f59e0b;
border-radius: 10px;
padding: 10px 14px;
transition: box-shadow .2s;
}
.global-discount-block:focus-within {
box-shadow: 0 0 0 3px rgba(245,158,11,.15);
}
.global-discount-label {
font-size: 11px;
font-weight: 700;
color: #92400e;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 5px;
text-transform: uppercase;
letter-spacing: .4px;
}
.global-discount-inputs {
display: flex;
align-items: center;
gap: 8px;
}
.global-discount-input {
flex: 1;
min-width: 0;
height: 36px;
border: 1.5px solid #f59e0b;
border-radius: 8px;
font-size: 15px;
font-weight: 700;
text-align: right;
padding: 0 10px;
background: #fff;
color: #1e293b;
outline: none;
transition: border-color .18s, box-shadow .18s;
}
.global-discount-input:focus {
border-color: #d97706;
box-shadow: 0 0 0 2px rgba(217,119,6,.2);
}
.global-discount-input::placeholder { color: #d1d5db; font-weight: 400; }
.global-discount-unit {
font-size: 13px;
color: #92400e;
font-weight: 700;
white-space: nowrap;
}
.global-discount-amount {
font-size: 12px;
font-weight: 700;
color: #dc2626;
margin-left: auto;
white-space: nowrap;
}
/* Total net */
.total-net-row {
display: flex;
justify-content: space-between;
align-items: center;
background: #ecfdf5;
border: 1.5px solid #10b981;
border-radius: 10px;
padding: 10px 14px;
margin: 6px 0 0;
animation: fadeSlideIn .2s ease;
}
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.total-net-label {
font-size: 12px;
font-weight: 700;
color: #065f46;
text-transform: uppercase;
letter-spacing: .4px;
}
.total-net-value {
font-size: 20px;
font-weight: 800;
color: #065f46;
}
/* ===================== ACTION BUTTONS ===================== */
.cart-actions {
padding: 12px 16px 16px;
}
.btn-primary-action {
width: 100%;
min-height: 54px;
padding: 15px;
background: var(--pos-success);
color: #fff;
border: none;
border-radius: var(--pos-radius);
font-size: 16.5px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
transition: background .18s, transform .12s;
margin-bottom: 10px;
}
.btn-primary-action:hover { background: #047857; }
.btn-primary-action:active { transform: scale(.98); }
.action-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 8px;
}
.btn-action {
min-height: 48px;
padding: 12px 8px;
border: 1px solid var(--pos-gray-200);
border-radius: 10px;
font-size: 13.5px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
transition: all .15s;
background: #fff;
color: var(--pos-gray-600);
}
.btn-action:active { transform: scale(.96); }
.btn-action:hover { background: var(--pos-gray-50); border-color: var(--pos-gray-400); }
.btn-action.cash { border-color: #a7f3d0; background: #ecfdf5; color: #047857; }
.btn-action.cash:hover { background: #d1fae5; }
.btn-action.unpaid { border-color: #fcd34d; background: var(--pos-warning-light); color: var(--pos-warning); }
.btn-action.unpaid:hover { background: #fef3c7; }
.btn-action.danger { border-color: #fecaca; background: #fef2f2; color: var(--pos-danger); }
.btn-action.danger:hover { background: #fee2e2; }
.btn-action.client { border-color: #bfdbfe; background: #eff6ff; color: var(--pos-primary); }
.btn-action.client:hover { background: #dbeafe; }
.btn-action.add-prod { border-color: #c7d2fe; background: #eef2ff; color: #4338ca; }
.btn-action.add-prod:hover { background: #e0e7ff; }
.btn-action.add-cat { border-color: var(--pos-gray-200); background: var(--pos-gray-50); color: var(--pos-gray-600); }
/* ===================== MODALS ===================== */
.pos-modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(15,23,42,.45);
z-index: 1000;
align-items: center;
justify-content: center;
}
.pos-modal-overlay.open { display: flex !important; }
.pos-modal {
background: #fff;
border-radius: 14px;
width: 100%;
max-width: 440px;
max-height: 90vh;
margin: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,.2);
overflow: hidden;
display: flex;
flex-direction: column;
}
.pos-modal-header {
background: var(--pos-primary);
color: #fff;
padding: 16px 20px;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.pos-modal-close {
background: rgba(255,255,255,.15);
border: none;
color: #fff;
width: 40px;
height: 40px;
border-radius: 10px;
cursor: pointer;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.pos-modal-body { padding: 20px; overflow-y: auto; flex: 1; min-height: 0; }
/* Cash modal */
.cash-amount-box {
background: var(--pos-gray-50);
border: 1px solid var(--pos-gray-200);
border-radius: 10px;
padding: 14px 16px;
margin-bottom: 12px;
text-align: center;
}
.cash-amount-box.highlight {
background: #fffbeb;
border-color: var(--pos-warning-border);
}
.cash-label {
font-size: 12px;
color: var(--pos-gray-400);
font-weight: 500;
display: block;
margin-bottom: 6px;
}
.cash-value {
font-size: 26px;
font-weight: 800;
color: var(--pos-gray-800);
}
.cash-input {
width: 100%;
border: 2px solid var(--pos-gray-200);
border-radius: 8px;
padding: 10px;
font-size: 26px;
font-weight: 800;
text-align: center;
color: var(--pos-gray-800);
outline: none;
transition: border-color .18s;
background: #fff;
}
.cash-input:focus { border-color: var(--pos-primary); }
.exact-amount-btn {
width: 100%;
min-height: 48px;
padding: 12px;
margin-top: 10px;
background: var(--pos-success-light);
border: 1.5px solid var(--pos-success);
border-radius: 10px;
font-size: 15px;
font-weight: 700;
color: var(--pos-success);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
transition: all .15s;
}
.exact-amount-btn:hover { background: #a7f3d0; }
.exact-amount-btn:active { transform: scale(.97); }
.quick-btns {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin: 14px 0;
}
.quick-btn {
min-height: 44px;
min-width: 56px;
padding: 10px 16px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 10px;
font-size: 15px;
font-weight: 700;
color: var(--pos-primary);
cursor: pointer;
transition: all .15s;
}
.quick-btn:hover { background: #dbeafe; }
.quick-btn:active { transform: scale(.95); }
.modal-action-row {
display: flex;
gap: 10px;
margin-top: 16px;
}
.btn-modal-cancel {
flex: 1;
min-height: 48px;
padding: 13px;
background: var(--pos-gray-100);
border: 1px solid var(--pos-gray-200);
border-radius: 10px;
font-size: 14.5px;
font-weight: 600;
color: var(--pos-gray-600);
cursor: pointer;
transition: all .15s;
}
.btn-modal-cancel:hover { background: var(--pos-gray-200); }
.btn-modal-confirm {
flex: 2;
min-height: 48px;
padding: 13px;
background: var(--pos-success);
border: none;
border-radius: 10px;
font-size: 15.5px;
font-weight: 700;
color: #fff;
cursor: pointer;
transition: background .15s;
}
.btn-modal-confirm:hover { background: #047857; }
/* Client modal */
.client-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 55vh;
overflow-y: auto;
}
.client-item {
min-height: 48px;
padding: 14px 16px;
border-bottom: 1px solid var(--pos-gray-100);
cursor: pointer;
font-size: 14.5px;
color: var(--pos-gray-800);
display: flex;
justify-content: space-between;
align-items: center;
transition: background .15s;
}
.client-item:hover { background: var(--pos-gray-50); }
.client-item::after { content: "›"; color: var(--pos-gray-400); font-size: 18px; }
/* Scrollbars */
.pos-products::-webkit-scrollbar,
.cart-body::-webkit-scrollbar,
.products-scroll::-webkit-scrollbar { width: 5px; }
.pos-products::-webkit-scrollbar-thumb,
.cart-body::-webkit-scrollbar-thumb,
.products-scroll::-webkit-scrollbar-thumb { background: var(--pos-gray-200); border-radius: 3px; }
/* ===================== RESPONSIVE ===================== */
/* Grands ecrans : un peu plus d'air pour le panier */
@media (min-width: 1400px) {
.pos-wrapper { grid-template-columns: 1fr clamp(460px, 32vw, 600px); }
}
/* Ecrans moyens / petits laptops */
@media (max-width: 1199px) {
.pos-wrapper { grid-template-columns: 1fr clamp(380px, 36vw, 460px); }
.product-grid { grid-template-columns: repeat(auto-fill, minmax(135px, 1fr)); }
}
/* Tablettes en mode paysage */
@media (max-width: 991px) {
.pos-wrapper { grid-template-columns: 1fr clamp(320px, 40vw, 380px); }
.product-grid { grid-template-columns: repeat(auto-fill, minmax(115px, 1fr)); }
}
/* Tablettes portrait & mobiles : panier en tiroir plein ecran */
@media (max-width: 767px) {
.pos-wrapper { grid-template-columns: 1fr; }
.pos-products { grid-column: 1 / -1; }
.product-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
.pos-cart {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: min(88vw, 380px);
border-left: none;
box-shadow: -8px 0 24px rgba(0,0,0,.2);
transform: translateX(100%);
transition: transform .25s ease;
z-index: 950;
}
.pos-cart.mobile-open { transform: translateX(0); }
.cart-close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin-left: auto;
background: rgba(255,255,255,.15);
border: none;
border-radius: 10px;
color: #fff;
font-size: 16px;
cursor: pointer;
flex-shrink: 0;
}
.cart-fab { display: flex; }
.cart-backdrop.open { display: block; }
/* Header : on laisse les boutons passer à la ligne plutôt que de les
couper hors écran — aucun élément de navigation ne doit devenir
inaccessible faute de place. */
.pos-header {
--pos-header-h: 60px;
flex-wrap: wrap;
height: auto;
min-height: var(--pos-header-h);
padding: 10px 14px;
row-gap: 8px;
}
.header-client { flex: 0 1 auto; }
.header-right {
flex: 1 1 100%;
flex-wrap: wrap;
justify-content: flex-end;
row-gap: 8px;
}
.btn-dashboard .dashboard-label { display: none; }
.btn-dashboard { padding: 10px; min-width: 44px; justify-content: center; }
.total-display { font-size: 16px; padding: 7px 14px; min-height: 40px; }
/* Grille produits : cartes plus petites pour en montrer davantage,
et marge basse suffisante pour ne jamais passer sous le bouton
panier flottant. */
.products-scroll { padding: 12px; padding-bottom: 104px; }
.product-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 10px; }
.product-img { height: 76px; }
.product-body { padding: 8px; }
.product-name { font-size: 11px; margin-bottom: 4px; }
.product-price { font-size: 12.5px; }
.product-price-buy { font-size: 9px; }
/* Barre de catégories : navigation tactile fluide, aucune pastille
tronquée en bord d'écran. */
.category-bar { padding: 8px 12px; gap: 6px; scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch; }
.category-bar .cat-pill { scroll-snap-align: start; }
.category-bar .cat-pill:last-child { margin-right: 4px; }
}
@media (max-width: 420px) {
.product-grid { grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 8px; }
.product-img { height: 64px; }
.client-name { max-width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}
/* ===== Contexte de commande (attente, tables, restaurant) ===== */
.order-context { display:flex; flex-wrap:wrap; gap:6px; align-items:center; justify-content:space-between; padding:8px 12px; background:var(--pos-gray-50); border-bottom:1px solid var(--pos-gray-200); }
.order-context-links { display:flex; gap:6px; }
.ctx-btn { display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:8px; border:1px solid var(--pos-gray-200); background:#fff; color:var(--pos-gray-800); font-size:12.5px; font-weight:600; text-decoration:none; }
.ctx-btn:hover { border-color:var(--pos-primary); color:var(--pos-primary); }
.ctx-btn.ctx-tables { border-color:#bfdbfe; background:#eff6ff; color:var(--pos-primary); }
.ctx-count { background:var(--pos-warning); color:#fff; border-radius:10px; padding:0 7px; font-size:11px; }
.order-context-form { display:flex; gap:6px; align-items:center; margin:0; }
.ctx-select { border:1px solid var(--pos-gray-200); border-radius:8px; padding:5px 8px; font-size:12.5px; background:#fff; }
.ctx-info { font-size:12.5px; color:var(--pos-gray-600); font-weight:600; }
.ctx-couverts { display:inline-flex; align-items:center; gap:4px; margin:0; font-size:12px; color:var(--pos-gray-600); }
.ctx-couverts input { width:44px; border:1px solid var(--pos-gray-200); border-radius:8px; padding:4px 6px; font-size:12.5px; }
.line-options { margin-top:6px; }
.line-options summary { font-size:11.5px; color:var(--pos-gray-600); cursor:pointer; user-select:none; list-style:none; display:inline-flex; align-items:center; gap:4px; }
.line-options summary::-webkit-details-marker { display:none; }
.line-options summary::before { content:'▸'; font-size:10px; transition:transform .15s; }
.line-options[open] summary::before { transform:rotate(90deg); }
.cart-item-note { font-size:11.5px; color:#7c3aed; margin-top:3px; }
.kitchen-badge { display:inline-flex; align-items:center; gap:3px; font-size:10.5px; color:#b45309; background:#fef3c7; border-radius:6px; padding:0 5px; margin-left:4px; }
.note-input { width:100%; text-align:left; }
.btn-action.card { border-color:#c7d2fe; background:#eef2ff; color:#3730a3; }
.btn-action.card:hover { background:#e0e7ff; }
.btn-action.kitchen { border-color:#fed7aa; background:#fff7ed; color:#c2410c; }
.btn-action.kitchen.pending { box-shadow:0 0 0 2px #fb923c inset; }
.btn-action.hold { border-color:#ddd6fe; background:#f5f3ff; color:#6d28d9; }
.hold-form { display:flex; gap:6px; margin:0 0 8px; }
.btn-action.kitchen, .btn-action.hold, .order-context + .cart-body ~ .cart-footer .btn-action.client { min-height:42px; padding:9px 8px; }
.hold-input { flex:1; min-width:0; border:1px solid var(--pos-gray-200); border-radius:8px; padding:6px 8px; font-size:12.5px; }
.no-cash-right { font-size:12.5px; color:var(--pos-gray-600); background:var(--pos-gray-50); border:1px dashed var(--pos-gray-200); border-radius:8px; padding:8px 10px; margin-bottom:6px; }
</style>
{# ========== HIDDEN CLIENT ID ========== #}
<input type="hidden" id="selectedClientId" value="">
{% include 'caisse/_flashes.html.twig' %}
<div class="pos-wrapper">
{# ========== HEADER ========== #}
<header class="pos-header">
<div class="header-client">
<div class="client-badge" id="customerBtn">
<div class="client-avatar" id="clientAvatarText">C</div>
<span class="client-name" id="clientNameDisplay">Client Comptant</span>
</div>
</div>
<div class="header-right">
{% if sessionCaisse %}
<span class="btn-dashboard" style="background:rgba(255,255,255,.14);cursor:default;" title="Caisse ouverte par {{ sessionCaisse.ouvertParNom }}">
<i class="fas fa-lock-open" style="font-size:15px;"></i>
<span class="dashboard-label">Caisse ouverte depuis {{ sessionCaisse.dateOuverture|date('H:i') }} — fond {{ sessionCaisse.fondOuverture|number_format(3, '.', '') }}</span>
</span>
{% if canCloture %}
<a href="{{ path('caisse_cloture_fermer', {'id': sessionCaisse.id}) }}" class="btn-dashboard" style="background:#dc2626;border-color:rgba(255,255,255,.3);">
<i class="fas fa-lock" style="font-size:15px;"></i>
<span class="dashboard-label">Clôturer la caisse</span>
</a>
{% endif %}
{% else %}
<a href="{{ path('caisse_cloture_ouvrir') }}" class="btn-dashboard" style="background:#059669;border-color:rgba(255,255,255,.3);">
<i class="fas fa-unlock" style="font-size:15px;"></i>
<span class="dashboard-label">Ouvrir la caisse</span>
</a>
{% endif %}
{% if canVoirRapports %}
<a href="{{ path('caisse_rapports_vendeurs') }}" class="btn-dashboard">
<i class="fas fa-chart-column" style="font-size:15px;"></i>
<span class="dashboard-label">Mes ventes</span>
</a>
{% endif %}
{% if canReparations %}
<a href="{{ path('reparation_index') }}" class="btn-dashboard">
<i class="fas fa-screwdriver-wrench" style="font-size:15px;"></i>
<span class="dashboard-label">Réparations</span>
</a>
{% endif %}
{% if not isSalarie %}
<a href="{{ path('dashboard') }}" class="btn-dashboard">
<i class="fas fa-tachometer-alt" style="font-size:15px;"></i>
<span class="dashboard-label">Tableau de bord</span>
</a>
{% endif %}
<div class="total-display" id="totalPrice">{{ totalTTC|number_format(3, '.', '') }} TND</div>
{% if principalNom %}
<div class="user-session-chip" title="{{ isSalarie ? 'Salarié connecté' : 'Compte connecté' }}">
<div class="user-session-avatar">{{ principalNom|slice(0, 1)|upper }}</div>
<span class="user-session-name">{{ principalNom }}</span>
<a href="{{ path('app_logout') }}" class="user-session-logout" id="shiftLogoutBtn"
data-panier-count="{{ panier|length }}"
title="{{ isSalarie ? 'Terminer mon service / changer de salarié' : 'Se déconnecter' }}">
<i class="fas fa-right-from-bracket"></i>
</a>
</div>
{% endif %}
<div class="pos-brand" title="Mazego.fr — Logiciel de caisse">
<span class="pos-brand-text">Mazego<span class="pos-brand-tld">.fr</span></span>
</div>
</div>
</header>
{# ========== PRODUCTS SECTION ========== #}
<div class="pos-products">
{# Categories #}
<div class="category-bar">
<a href="{{ path('point_vente_index') }}" class="cat-pill {{ selectedCategory is null ? 'active' : '' }}">
Tous
</a>
{% for categorie in categories %}
<a href="{{ path('point_vente_index', {'categorie': categorie.id}) }}"
class="cat-pill {{ selectedCategory == categorie.id ? 'active' : '' }}">
{{ categorie.nom }}
</a>
{% endfor %}
</div>
{# Search #}
<div class="search-bar">
<div class="search-inner">
<i class="fas fa-search"></i>
<form method="get" action="{{ path('point_vente_index') }}" id="searchForm">
<input type="text" name="search" class="search-input"
placeholder="Rechercher un produit ou scanner un code-barres..."
value="{{ searchTerm }}" id="searchInput" autocomplete="off">
</form>
</div>
</div>
{# Formulaire caché : ajout direct au panier via scan de code-barres #}
<form id="scanBarcodeForm" action="{{ path('point_vente_scan_barcode') }}" method="POST" style="display:none;">
<input type="hidden" name="code_barre" id="scanBarcodeInput">
<input type="hidden" name="_token" value="{{ csrf_token('scan_barcode') }}">
</form>
{# Products grid #}
<div class="products-scroll">
<div class="product-grid">
{% for produit in produits %}
{% set prixAffichage = produit.prixVente ?: produit.puht %}
<div class="product-card" onclick="addToCartWithBeep('add-to-cart-{{ produit.id }}')">
<div class="product-img"
style="background-image:url('{% if produit.imageUrl %}{{ asset('uploads/' ~ produit.imageUrl) }}{% else %}{{ asset('uploads/images/default-product.png') }}{% endif %}')">
{% if produit.discountRate and produit.discountRate > 0 %}
<span class="discount-badge">-{{ produit.discountRate }}%</span>
{% endif %}
</div>
<div class="product-body">
<div class="product-name">{{ produit.description }}</div>
<div class="product-price">{{ prixAffichage|number_format(3, '.', '') }} TND</div>
{% if produit.npuht %}
<div class="product-price-buy">Achat : {{ produit.npuht|number_format(3, '.', '') }}</div>
{% endif %}
</div>
<form id="add-to-cart-{{ produit.id }}"
action="{{ path('point_vente_add_to_cart', {'id': produit.id}) }}"
method="POST" style="display:none;">
<input type="hidden" name="categorie" value="{{ selectedCategory }}">
<input type="hidden" name="search" value="{{ searchTerm }}">
</form>
</div>
{% else %}
<div class="no-products">
<i class="fas fa-box-open" style="font-size:32px;margin-bottom:8px;"></i>
<div>Aucun produit trouvé</div>
</div>
{% endfor %}
</div>
</div>
</div>
{# ========== CART SECTION ========== #}
{% set modeResto = entreprise and entreprise.caisseModeRestaurant %}
{% set enAttenteActif = entreprise is null or entreprise.caisseCommandesEnAttente %}
<div class="pos-cart" id="posCart">
<div class="cart-header">
<i class="fas fa-shopping-cart"></i>
{% if commande and commande.table %}
{{ commande.table.libelle }}
{% elseif commande and commande.libelle %}
{{ commande.libelle }}
{% else %}
Panier
{% endif %}
{% if panier|length > 0 %}
<span style="background:rgba(255,255,255,.25);border-radius:10px;padding:1px 8px;font-size:11px;margin-left:4px;">
{{ panier|length }} article{{ panier|length > 1 ? 's' : '' }}
</span>
{% endif %}
<button type="button" class="cart-close-btn" id="cartCloseBtn" aria-label="Fermer le panier">
<i class="fas fa-times"></i>
</button>
</div>
{# Barre de contexte : tables, commandes en attente, type de commande #}
{% if enAttenteActif or modeResto %}
<div class="order-context">
<div class="order-context-links">
{% if entreprise and entreprise.caisseGestionTables %}
<a href="{{ path('point_vente_tables') }}" class="ctx-btn ctx-tables"><i class="fas fa-chair"></i> Tables</a>
{% endif %}
{% if enAttenteActif %}
<a href="{{ path('point_vente_en_attente') }}" class="ctx-btn">
<i class="fas fa-clock"></i> En attente
{% if nbEnAttente > 0 %}<span class="ctx-count">{{ nbEnAttente }}</span>{% endif %}
</a>
{% endif %}
</div>
{% if modeResto %}
<form method="POST" action="{{ path('point_vente_commande_infos') }}" class="order-context-form">
{% if commande and commande.table %}
<span class="ctx-info"><i class="fas fa-utensils"></i> Sur place</span>
{% if tablesTransfert is not empty %}
<select class="ctx-select" aria-label="Transférer vers" form="transfertForm" name="table_id"
onchange="if (this.value && confirm('Transférer la commande vers la table choisie ? Si elle est occupée, les commandes seront fusionnées.')) document.getElementById('transfertForm').submit(); else this.value='';">
<option value="">Transférer vers…</option>
{% for t in tablesTransfert %}
{% if t.id != commande.table.id %}<option value="{{ t.id }}">{{ t.salle.nom }} · {{ t.libelle }}</option>{% endif %}
{% endfor %}
</select>
{% endif %}
{% else %}
<select name="type_commande" class="ctx-select" onchange="this.form.submit()" aria-label="Type de commande">
{% for code, libelle in typesCommande %}
<option value="{{ code }}" {{ (commande ? commande.typeCommande : 'sur_place') == code ? 'selected' }}>{{ libelle }}</option>
{% endfor %}
</select>
{% endif %}
<label class="ctx-couverts" title="Nombre de couverts">
<i class="fas fa-user-group"></i>
<input type="number" name="nb_couverts" min="0" max="99" value="{{ commande ? commande.nbCouverts : '' }}"
placeholder="–" onchange="this.form.submit()">
</label>
</form>
{% endif %}
</div>
{% endif %}
<form id="transfertForm" method="POST" action="{{ path('point_vente_transferer') }}" style="display:none;"></form>
{# Cart items #}
<div class="cart-body">
{% if panier is empty %}
<div class="cart-empty">
<i class="fas fa-shopping-cart" style="font-size:36px;opacity:.25;"></i>
<span>Le panier est vide</span>
</div>
{% else %}
{% for item in panier %}
{% set ligne = item.ligne %}
<div class="cart-item {{ item.tauxRemise > 0 ? 'has-discount' : '' }}">
<div class="cart-item-top">
<div class="cart-item-name">
{{ ligne.libelle }}
{% if ligne.envoyeeCuisine %}
<span class="kitchen-badge" title="Envoyé en cuisine : {{ ligne.quantiteEnvoyeeCuisine }}">
<i class="fas fa-fire-burner"></i>{% if ligne.quantiteAEnvoyer > 0 %} +{{ ligne.quantiteAEnvoyer }}{% endif %}
</span>
{% endif %}
</div>
<div class="cart-item-total">{{ item.totalHT|number_format(3, '.', '') }}</div>
</div>
<div class="cart-item-meta">
{{ item.prixApresRemise|number_format(3, '.', '') }} × {{ ligne.quantite }} TND
{% if item.tauxRemise > 0 %}
· <span style="color:var(--pos-warning);font-weight:600;">-{{ item.tauxRemise }}%</span>
{% endif %}
</div>
{% if ligne.note %}
<div class="cart-item-note"><i class="fas fa-comment-dots"></i> {{ ligne.note }}</div>
{% endif %}
{# Quantity controls + remove #}
<div class="cart-item-controls">
<form action="{{ path('point_vente_update_cart', {'id': ligne.id}) }}" method="POST">
<input type="hidden" name="quantite" value="{{ ligne.quantite - 1 }}">
<button type="submit" class="qty-btn">−</button>
</form>
<span class="qty-display">{{ ligne.quantite }}</span>
<form action="{{ path('point_vente_update_cart', {'id': ligne.id}) }}" method="POST">
<input type="hidden" name="quantite" value="{{ ligne.quantite + 1 }}">
<button type="submit" class="qty-btn">+</button>
</form>
<form action="{{ path('point_vente_remove_from_cart', {'id': ligne.id}) }}" method="POST" style="margin-left:auto;">
<button type="submit" class="btn-remove"><i class="fas fa-trash-alt" style="font-size:11px;"></i></button>
</form>
</div>
{# Remise et note : repliées pour garder le panier compact #}
<details class="line-options" {{ item.tauxRemise > 0 and ligne.remiseManuelle is not null ? 'open' }}>
<summary>{{ modeResto ? 'Remise / note cuisine' : 'Remise' }}</summary>
<form action="{{ path('point_vente_apply_discount', {'id': ligne.id}) }}"
method="POST" class="discount-form">
<div class="discount-row">
<i class="fas fa-tag" style="font-size:10px;color:var(--pos-gray-400);"></i>
<input type="number" name="remise" class="discount-input-small"
value="{{ item.tauxRemise }}" min="0" max="100" step="0.1"
placeholder="Remise %">
<button type="submit" class="discount-submit-btn">Appliquer</button>
</div>
</form>
{% if modeResto %}
<form action="{{ path('point_vente_ligne_note', {'id': ligne.id}) }}" method="POST" class="note-form">
<div class="discount-row">
<i class="fas fa-comment-dots" style="font-size:10px;color:var(--pos-gray-400);"></i>
<input type="text" name="note" class="discount-input-small note-input" maxlength="255"
value="{{ ligne.note }}" placeholder="Note cuisine (sans oignon...)">
<button type="submit" class="discount-submit-btn">OK</button>
</div>
</form>
{% endif %}
</details>
</div>
{% endfor %}
{% endif %}
</div>
{# ========== FOOTER : SUMMARY + ACTIONS ========== #}
<div class="cart-footer">
<div class="cart-summary">
{# HT #}
<div class="summary-row">
<span>Sous-total HT</span>
<span>{{ totalPanier|number_format(3, '.', '') }} TND</span>
</div>
{# TVA par taux #}
{% for detail in detailsTva %}
<div class="summary-row">
<span>TVA {{ detail.taux }}%</span>
<span>{{ detail.montant|number_format(3, '.', '') }} TND</span>
</div>
{% else %}
<div class="summary-row">
<span>Total TVA</span>
<span>{{ totalTva|number_format(3, '.', '') }} TND</span>
</div>
{% endfor %}
{% if timbre > 0 %}
<div class="summary-row">
<span>Droit de timbre</span>
<span>{{ timbre|number_format(3, '.', '') }} TND</span>
</div>
{% endif %}
{# TTC #}
<div class="summary-row ttc-row">
<span>Total TTC</span>
<span id="total">{{ (totalTTC + timbre)|number_format(3, '.', '') }} TND</span>
</div>
{# Remise globale #}
<div class="global-discount-block">
<div class="global-discount-label">
<i class="fas fa-scissors"></i>
Remise globale (DT)
</div>
<div class="global-discount-inputs">
<input type="number"
id="global_discount"
name="global_discount"
min="0"
max="{{ totalTTC }}"
step="0.001"
placeholder="0.000"
class="global-discount-input"
autocomplete="off"
oninput="updateTotalNet(this.value)">
<span class="global-discount-unit">DT</span>
<span id="remise-display" class="global-discount-amount" style="display:none;"></span>
</div>
</div>
{# Total net (affiché si remise > 0) #}
<div class="total-net-row" id="total-net-block" style="display:none;">
<span class="total-net-label">TOTAL NET À PAYER</span>
<span class="total-net-value" id="total-net-value">0.000 TND</span>
</div>
</div>
{# ========== ACTION BUTTONS ========== #}
<div class="cart-actions">
{% if canEncaisser %}
{# Bouton principal #}
<button class="btn-primary-action" id="processPaymentBtn">
<i class="fas fa-receipt"></i>
Valider & imprimer ticket
</button>
{% endif %}
{# Formulaire ticket (caché) #}
<form method="POST" action="{{ path('point_vente_valider_commande_ticket') }}"
id="ticketForm" style="display:none;">
<input type="hidden" name="selectedClientId" id="selectedClientIdTicket" value="">
<input type="hidden" name="payment_method" value="espece">
<input type="hidden" name="montant_recu" value="">
<input type="hidden" name="monnaie_rendre" value="">
<input type="hidden" name="global_discount" id="ticketFormDiscount" value="0">
<input type="hidden" name="_token" value="{{ csrf_token('validate_order') }}">
</form>
{# Formulaire paiement (caché) #}
<form method="POST" action="{{ path('point_vente_valider_commande') }}"
id="paymentForm" style="display:none;">
<input type="hidden" name="selectedespeceClientId" id="selectedespeceClientId" value="">
<input type="hidden" name="payment_method" value="">
<input type="hidden" name="montant_recu" value="">
<input type="hidden" name="monnaie_rendre" value="">
<input type="hidden" name="global_discount" id="paymentFormDiscount" value="0">
<input type="hidden" name="_token" value="{{ csrf_token('validate_order') }}">
</form>
{% if canEncaisser %}
{# Row 1 : moyens de paiement #}
<div class="action-row" style="grid-template-columns:1fr 1fr 1fr;">
<button class="btn-action cash" onclick="openCashModal('normal')">
<i class="fas fa-money-bill-wave" style="font-size:15px;"></i>
Espèces
</button>
<button class="btn-action card" data-payment="carte">
<i class="fas fa-credit-card" style="font-size:15px;"></i>
Carte
</button>
<button class="btn-action unpaid" data-payment="nonPaye">
<i class="fas fa-times" style="font-size:15px;"></i>
Non payé
</button>
</div>
{% else %}
<div class="no-cash-right">
<i class="fas fa-circle-info"></i>
L'encaissement est fait par un responsable. Envoyez la commande en cuisine
ou mettez-la en attente / sur une table.
</div>
{% endif %}
{# Restaurant : cuisine, pré-addition, addition partagée #}
{% if modeResto and commande and not commande.vide %}
<div class="action-row" style="grid-template-columns:repeat({{ entreprise.caisseTicketCuisine ? 3 : 2 }}, 1fr);">
{% if entreprise.caisseTicketCuisine %}
<form method="POST" action="{{ path('point_vente_bon_cuisine') }}" target="_blank" style="display:contents;">
<button type="submit" class="btn-action kitchen {{ commande.articlesNonEnvoyesCuisine ? 'pending' : '' }}"
onclick="setTimeout(function(){ window.location.reload(); }, 800);">
<i class="fas fa-fire-burner" style="font-size:14px;"></i>
Cuisine
</button>
</form>
{% endif %}
<a href="{{ path('point_vente_pre_addition') }}" target="_blank" class="btn-action client" style="text-decoration:none;">
<i class="fas fa-file-invoice" style="font-size:14px;"></i>
Pré-addition
</a>
<a href="{{ path('point_vente_separer') }}" class="btn-action add-prod" style="text-decoration:none;">
<i class="fas fa-divide" style="font-size:14px;"></i>
Partager
</a>
</div>
{% endif %}
{# Mise en attente / table #}
{% if commande and not commande.vide and (enAttenteActif or (commande.table)) %}
<form method="POST" action="{{ path('point_vente_mettre_en_attente') }}" class="hold-form">
{% if commande.table %}
<button type="submit" class="btn-action hold" style="width:100%;">
<i class="fas fa-chair" style="font-size:14px;"></i>
Garder sur la {{ commande.table.libelle }} et revenir au plan
</button>
{% else %}
<input type="text" name="libelle" class="hold-input" maxlength="120"
value="{{ commande.libelle }}" placeholder="Nom (optionnel)">
<button type="submit" class="btn-action hold">
<i class="fas fa-pause" style="font-size:14px;"></i>
Mettre en attente
</button>
{% endif %}
</form>
{% endif %}
{# Row 2 : client + produit + catégorie #}
<div class="action-row" style="grid-template-columns:1fr 1fr 1fr;">
<button class="btn-action client" id="addClientBtn">
<i class="fas fa-user-plus" style="font-size:14px;"></i>
Client
</button>
<button class="btn-action add-prod"
data-bs-toggle="modal" data-bs-target="#addProductFromCaisseModal">
<i class="fas fa-plus-circle" style="font-size:14px;"></i>
Produit
</button>
<button class="btn-action add-cat"
data-bs-toggle="modal" data-bs-target="#addCategoryFromCaisseModal">
<i class="fas fa-tags" style="font-size:14px;"></i>
Catégorie
</button>
</div>
{# Vider le panier — séparé et discret #}
<button class="btn-action danger" style="width:100%;margin-top:4px;" onclick="confirmClearCart()">
<i class="fas fa-trash-alt" style="font-size:14px;"></i>
{{ commande and commande.table ? 'Libérer la table' : 'Vider le panier' }}
</button>
<form id="clearCartForm" method="POST"
action="{{ path('point_vente_vider_panier') }}" style="display:none;">
</form>
</div>
</div>
</div>
</div>
{# ========== PANIER MOBILE : BACKDROP + BOUTON FLOTTANT ========== #}
<div id="cartBackdrop" class="cart-backdrop"></div>
<button type="button" class="cart-fab" id="cartFab">
<i class="fas fa-shopping-cart"></i>
<span id="fabTotal">{{ totalTTC|number_format(3, '.', '') }} TND</span>
{% if panier|length > 0 %}
<span class="fab-count" id="fabCount">{{ panier|length }}</span>
{% endif %}
</button>
{# ========== MODAL PAIEMENT ESPÈCES ========== #}
<div id="cashPaymentModal" class="pos-modal-overlay">
<div class="pos-modal">
<div class="pos-modal-header">
<span><i class="fas fa-money-bill-wave me-2"></i>Paiement en espèces</span>
<button class="pos-modal-close" onclick="closeCashModal()">×</button>
</div>
<div class="pos-modal-body">
<div class="cash-amount-box">
<span class="cash-label">Total à payer</span>
<span class="cash-value" id="cash-total">0.000</span>
<span style="font-size:13px;color:var(--pos-gray-400);"> TND</span>
</div>
<div class="cash-amount-box" style="padding:10px 16px;">
<span class="cash-label">Montant reçu</span>
<input type="number" id="cash-received" class="cash-input"
placeholder="0.000" step="0.001" min="0"
oninput="calculateChange()">
</div>
<button type="button" class="exact-amount-btn" onclick="setExactAmount()">
<i class="fas fa-check-circle"></i>
Montant exact
</button>
<div class="quick-btns">
<button class="quick-btn" onclick="addAmount(100)">100</button>
<button class="quick-btn" onclick="addAmount(50)">50</button>
<button class="quick-btn" onclick="addAmount(20)">20</button>
<button class="quick-btn" onclick="addAmount(10)">10</button>
<button class="quick-btn" onclick="addAmount(5)">5</button>
<button class="quick-btn" onclick="addAmount(1)">1</button>
</div>
<div class="cash-amount-box highlight">
<span class="cash-label">Monnaie à rendre</span>
<span class="cash-value" id="cash-change" style="color:var(--pos-warning);">0.000</span>
<span style="font-size:13px;color:var(--pos-gray-400);"> TND</span>
</div>
<div class="modal-action-row">
<button class="btn-modal-cancel" onclick="closeCashModal()">Annuler</button>
<button class="btn-modal-confirm" onclick="confirmCashPayment()">
<i class="fas fa-check me-1"></i>Valider
</button>
</div>
</div>
</div>
</div>
{# ========== MODAL CLIENT ========== #}
<div id="customerModal" class="pos-modal-overlay">
<div class="pos-modal">
<div class="pos-modal-header">
<span><i class="fas fa-users me-2"></i>Sélectionner un client</span>
<button class="pos-modal-close" onclick="closeModal('customerModal')">×</button>
</div>
<div class="pos-modal-body" style="padding:0;">
<ul class="client-list">
{% for client in clients %}
<li class="client-item" data-client-id="{{ client.id }}"
data-client-name="{{ client.nom }} {{ client.prenom }}">
<span>{{ client.nom }} {{ client.prenom }}</span>
<span style="font-size:11px;color:var(--pos-gray-400);">{{ client.telephone }}</span>
</li>
{% else %}
<li style="padding:20px;text-align:center;font-size:13px;color:var(--pos-gray-400);">
Aucun client trouvé
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{# ========== MODAL AJOUT CLIENT ========== #}
<div id="addClientModal" class="pos-modal-overlay">
<div class="pos-modal" style="max-width:500px;">
<div class="pos-modal-header">
<span><i class="fas fa-user-plus me-2"></i>Nouveau client</span>
<button class="pos-modal-close" onclick="closeModal('addClientModal')">×</button>
</div>
<div class="pos-modal-body">
{{ form_start(form, {'attr': {'action': path('client_new_modal')}}) }}
{{ form_widget(form) }}
<div class="modal-action-row">
<button type="button" class="btn-modal-cancel" onclick="closeModal('addClientModal')">Annuler</button>
<button type="submit" class="btn-modal-confirm">Enregistrer</button>
</div>
{{ form_end(form) }}
</div>
</div>
</div>
{# ========== MODAL CAISSE FERMÉE ========== #}
<div id="noCaisseSessionModal" class="pos-modal-overlay">
<div class="pos-modal" style="max-width:420px;">
<div class="pos-modal-header" style="background:var(--pos-warning);">
<span><i class="fas fa-triangle-exclamation me-2"></i>Caisse fermée</span>
<button class="pos-modal-close" onclick="closeModal('noCaisseSessionModal')">×</button>
</div>
<div class="pos-modal-body" style="text-align:center;">
<i class="fas fa-cash-register" style="font-size:38px;color:var(--pos-warning);margin-bottom:12px;"></i>
<p style="font-size:14.5px;color:var(--pos-gray-800);margin-bottom:0;">
Tu dois d'abord <strong>ouvrir la caisse</strong> avant de pouvoir encaisser des ventes.
</p>
<div class="modal-action-row">
<button type="button" class="btn-modal-cancel" onclick="closeModal('noCaisseSessionModal')">Annuler</button>
<a href="{{ path('caisse_cloture_ouvrir') }}" class="btn-modal-confirm" style="text-decoration:none;text-align:center;">
Ouvrir la caisse
</a>
</div>
</div>
</div>
</div>
{# ========== MODAL NOUVEAU PRODUIT ========== #}
<div class="modal fade" id="addProductFromCaisseModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered mw-650px">
<div class="modal-content">
<div class="modal-header" style="background:var(--pos-primary);color:#fff;">
<h5 class="modal-title">Nouveau produit</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="{{ path('app_produit_new_from_caisse') }}" enctype="multipart/form-data">
<div class="modal-body">
{{ render(path('app_produit_new_from_caisse')) }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annuler</button>
</div>
</form>
</div>
</div>
</div>
{# ========== MODAL NOUVELLE CATÉGORIE ========== #}
<div class="modal fade" id="addCategoryFromCaisseModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" style="background:var(--pos-primary);color:#fff;">
<h5 class="modal-title">Nouvelle catégorie</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="{{ path('app_categorie_new_from_caisse') }}">
<div class="modal-body">
{{ render(path('app_categorie_new_from_caisse')) }}
</div>
</form>
</div>
</div>
</div>
{# ========== JAVASCRIPT ========== #}
<script>
/* ---- CONSTANTES ---- */
const TTC_BRUT = {{ totalTTC + timbre }};
const CAISSE_SESSION_OUVERTE = {{ sessionCaisse ? 'true' : 'false' }};
/* ============================================================
SIGNAL SONORE (bip caisse à l'ajout d'un produit au panier)
============================================================ */
var __caissePosAudioCtx = null;
function playCaisseBeep() {
try {
var AudioCtx = window.AudioContext || window.webkitAudioContext;
if (!AudioCtx) return;
if (!__caissePosAudioCtx) __caissePosAudioCtx = new AudioCtx();
if (__caissePosAudioCtx.state === 'suspended') __caissePosAudioCtx.resume();
var ctx = __caissePosAudioCtx;
var osc = ctx.createOscillator();
var gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'square';
osc.frequency.setValueAtTime(1568, ctx.currentTime);
gain.gain.setValueAtTime(0.0001, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.2, ctx.currentTime + 0.01);
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.11);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.12);
} catch (e) {
/* Audio non disponible (navigateur, permissions...) : on ignore silencieusement */
}
}
/**
* Joue le bip caisse puis soumet le formulaire d'ajout au panier,
* avec un très court délai pour laisser le son démarrer avant le
* rechargement de page.
*/
function addToCartWithBeep(formId) {
if (!CAISSE_SESSION_OUVERTE) {
openModal('noCaisseSessionModal');
return;
}
playCaisseBeep();
var form = document.getElementById(formId);
if (form) {
window.setTimeout(function () { form.submit(); }, 90);
}
}
/* ============================================================
REMISE GLOBALE
============================================================ */
function getEffectiveTotal() {
const netBlock = document.getElementById('total-net-block');
if (netBlock && netBlock.style.display !== 'none') {
const raw = document.getElementById('total-net-value').textContent.replace(' TND','').trim();
return parseFloat(raw) || 0;
}
return TTC_BRUT;
}
function syncDiscount() {
const val = parseFloat(document.getElementById('global_discount').value) || 0;
document.getElementById('paymentFormDiscount').value = val.toFixed(3);
document.getElementById('ticketFormDiscount').value = val.toFixed(3);
}
function updateTotalNet(remise) {
const r = parseFloat(remise) || 0;
const net = Math.max(0, TTC_BRUT - r);
const netBlock = document.getElementById('total-net-block');
const netValue = document.getElementById('total-net-value');
const remiseDisplay = document.getElementById('remise-display');
const cashTotal = document.getElementById('cash-total');
if (r > 0) {
netBlock.style.display = 'flex';
netValue.textContent = net.toFixed(3) + ' TND';
if (remiseDisplay) {
remiseDisplay.textContent = '– ' + r.toFixed(3) + ' DT';
remiseDisplay.style.display = 'inline';
}
if (cashTotal) cashTotal.textContent = net.toFixed(3);
} else {
netBlock.style.display = 'none';
if (remiseDisplay) remiseDisplay.style.display = 'none';
if (cashTotal) cashTotal.textContent = TTC_BRUT.toFixed(3);
}
syncDiscount();
}
/* ============================================================
MODALE ESPÈCES
============================================================ */
function openCashModal(mode) {
const modal = document.getElementById('cashPaymentModal');
const effective = getEffectiveTotal();
modal.dataset.mode = mode || 'normal';
document.getElementById('cash-total').textContent = effective.toFixed(3);
document.getElementById('cash-received').value = '';
document.getElementById('cash-change').textContent = '0.000';
modal.classList.add('open');
setTimeout(() => document.getElementById('cash-received').focus(), 120);
}
function closeCashModal() {
document.getElementById('cashPaymentModal').classList.remove('open');
}
function calculateChange() {
const total = parseFloat(document.getElementById('cash-total').textContent) || 0;
const received = parseFloat(document.getElementById('cash-received').value) || 0;
const change = Math.max(0, received - total);
document.getElementById('cash-change').textContent = change.toFixed(3);
}
function addAmount(amount) {
const input = document.getElementById('cash-received');
const current = parseFloat(input.value) || 0;
input.value = (current + amount).toFixed(3);
calculateChange();
}
function setExactAmount() {
const total = parseFloat(document.getElementById('cash-total').textContent) || 0;
document.getElementById('cash-received').value = total.toFixed(3);
calculateChange();
}
function confirmCashPayment() {
const modal = document.getElementById('cashPaymentModal');
const mode = modal.dataset.mode || 'normal';
const total = parseFloat(document.getElementById('cash-total').textContent) || 0;
const received = parseFloat(document.getElementById('cash-received').value) || 0;
if (!received || isNaN(received)) {
alert('Veuillez entrer un montant reçu.');
return;
}
if (received < total) {
if (!confirm('Le montant reçu est inférieur au total. Continuer ?')) return;
}
const change = Math.max(0, received - total);
syncDiscount();
updatePaymentForms(received, change);
closeCashModal();
if (mode === 'ticket') {
processTicketPayment();
} else {
document.getElementById('paymentForm').submit();
}
}
function updatePaymentForms(received, change) {
const montantRecu = received.toFixed(3);
const monnaieRendre = change.toFixed(3);
const pf = document.getElementById('paymentForm');
pf.querySelector('[name="payment_method"]').value = 'espece';
pf.querySelector('[name="montant_recu"]').value = montantRecu;
pf.querySelector('[name="monnaie_rendre"]').value = monnaieRendre;
const tf = document.getElementById('ticketForm');
tf.querySelector('[name="payment_method"]').value = 'espece';
tf.querySelector('[name="montant_recu"]').value = montantRecu;
tf.querySelector('[name="monnaie_rendre"]').value = monnaieRendre;
}
/* ============================================================
BOUTON VALIDER + TICKET
============================================================ */
document.getElementById('processPaymentBtn')?.addEventListener('click', function () {
openCashModal('ticket');
});
function processTicketPayment() {
const form = document.getElementById('ticketForm');
// Synchroniser le client sélectionné dans le formulaire ticket
const clientId = document.getElementById('selectedClientId')?.value || '';
const clientName = document.getElementById('clientNameDisplay')?.textContent || '';
const ticketClientField = document.getElementById('selectedClientIdTicket');
if (ticketClientField && clientId) {
ticketClientField.value = clientId;
}
const formData = new FormData(form);
// Garantie supplémentaire : écraser dans le formData directement
if (clientId) formData.set('selectedClientId', clientId);
if (clientName && clientName !== 'Client Comptant') formData.set('nomClientChoisi', clientName);
fetch(form.action, {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => { if (!r.ok) return r.json().then(e => { throw e; }); return r.json(); })
.then(data => {
if (data.success) {
printTicket(data.ticketData);
setTimeout(() => { window.location.href = "{{ path('point_vente_index') }}"; }, 1200);
} else {
throw new Error(data.message || 'Erreur lors de la validation');
}
})
.catch(err => alert('Erreur : ' + (err.message || 'Communication serveur échouée')));
}
/* ============================================================
NON PAYÉ et autres méthodes
============================================================ */
document.querySelectorAll('[data-payment]').forEach(btn => {
btn.addEventListener('click', function () {
const method = this.dataset.payment;
const form = document.getElementById('paymentForm');
form.querySelector('[name="payment_method"]').value = method;
syncDiscount();
if (confirm('Confirmer le paiement "' + this.textContent.trim() + '" ?')) {
form.submit();
}
});
});
/* ============================================================
IMPRESSION TICKET
============================================================ */
function printTicket(ticketData) {
const win = window.open('', '_blank', 'width=420,height=650,menubar=no,toolbar=no,location=no');
if (!win) { alert('Autorisez les pop-ups pour imprimer le ticket.'); return; }
const td = {
nomEntreprise: ticketData.nomEntreprise || 'VOTRE ENTREPRISE',
date: ticketData.date || new Date().toLocaleDateString('fr-FR'),
reference: ticketData.reference || 'N/A',
nomClient: ticketData.nomClient || 'Client Comptant',
items: ticketData.items || [],
totalHT: parseFloat(ticketData.totalHT) || 0,
tva: parseFloat(ticketData.tva) || 0,
totalTTC: parseFloat(ticketData.totalTTC) || 0,
montantRecu: parseFloat(ticketData.montantRecu)|| 0,
monnaieRendre: parseFloat(ticketData.monnaieRendre) || 0,
detailsTva: ticketData.detailsTva || {},
globalDiscount:parseFloat(ticketData.globalDiscount) || 0,
totalRemise: parseFloat(ticketData.totalRemise) || 0,
timbre: parseFloat(ticketData.timbre) || 0
};
let tvaHtml = '';
for (const [, detail] of Object.entries(td.detailsTva)) {
tvaHtml += `
<div class="row"><span>Base HT ${detail.taux}% :</span><span>${parseFloat(detail.base).toFixed(3)} TND</span></div>
<div class="row"><span>TVA ${detail.taux}% :</span><span>${parseFloat(detail.montant).toFixed(3)} TND</span></div>`;
}
const discountSection = (td.globalDiscount > 0) ? `
<div class="row" style="color:#c00;font-weight:600;">
<span>Remise globale :</span><span>– ${td.globalDiscount.toFixed(3)} TND</span>
</div>
<div class="row final" style="color:#065f46;border-top:1px dashed #000;margin-top:4px;padding-top:4px;">
<span>TOTAL NET TTC :</span><span>${td.totalTTC.toFixed(3)} TND</span>
</div>` : `
<div class="row final"><span>TOTAL TTC :</span><span>${td.totalTTC.toFixed(3)} TND</span></div>`;
const timbreHtml = td.timbre > 0
? `<div class="row"><span>Timbre fiscal :</span><span>${td.timbre.toFixed(3)} TND</span></div>` : '';
const remiseLignesHtml = td.totalRemise > 0
? `<div class="row" style="color:#c00;"><span>Remises lignes :</span><span>– ${td.totalRemise.toFixed(3)} TND</span></div>` : '';
win.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
<title>Ticket ${td.reference}</title>
<style>
@page { size:80mm auto; margin:0; }
body { font-family:'Courier New',monospace; font-size:11px; margin:0; padding:0; width:80mm; }
.ticket { width:74mm; margin:0 auto; padding:4mm 3mm; }
.header { text-align:center; border-bottom:1px dashed #000; padding-bottom:6px; margin-bottom:6px; }
.company { font-size:15px; font-weight:900; letter-spacing:1px; text-transform:uppercase; }
.info { font-size:10px; margin:2px 0; }
.items { margin:6px 0; }
.item-head { display:flex; justify-content:space-between; font-weight:700; font-size:10px;
border-bottom:1px solid #000; padding-bottom:3px; margin-bottom:3px; }
.item-row { display:flex; justify-content:space-between; font-size:10px; margin-bottom:2px; }
.desc { flex:2; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; padding-right:4px; }
.qty { width:12%; text-align:center; }
.pu { width:20%; text-align:right; }
.tot { width:20%; text-align:right; }
.totals { border-top:1px dashed #000; border-bottom:1px dashed #000; padding:5px 0; margin:5px 0; }
.row { display:flex; justify-content:space-between; font-size:11px; margin-bottom:2px; }
.row.final { font-weight:900; font-size:13px; }
.payment { font-size:10px; margin:4px 0; }
.footer { text-align:center; border-top:1px dashed #000; padding-top:6px; margin-top:8px; font-size:9px; }
.merci { font-size:12px; font-weight:900; }
</style></head><body><div class="ticket">
<div class="header">
<div class="company">${td.nomEntreprise}</div>
<div class="info">${td.date}</div>
<div class="info">N° ${td.reference}</div>
<div class="info">Client : ${td.nomClient}</div>
</div>
<div class="items">
<div class="item-head">
<span class="desc">Article</span>
<span class="qty">Qté</span>
<span class="pu">P.U</span>
<span class="tot">Total</span>
</div>
${td.items.map(i => `
<div class="item-row">
<span class="desc" title="${i.description}">${i.description}</span>
<span class="qty">${i.quantite}</span>
<span class="pu">${parseFloat(i.prixUnitaire).toFixed(3)}</span>
<span class="tot">${parseFloat(i.total).toFixed(3)}</span>
</div>
${parseFloat(i.tauxRemise) > 0 ? `<div class="item-row" style="color:#c00;font-size:9px;"><span class="desc"> └ remise ${parseFloat(i.tauxRemise)}%</span></div>` : ''}
`).join('')}
</div>
<div class="totals">
${tvaHtml}
<div class="row"><span>Total HT :</span><span>${td.totalHT.toFixed(3)} TND</span></div>
<div class="row"><span>Total TVA :</span><span>${td.tva.toFixed(3)} TND</span></div>
${remiseLignesHtml}
${timbreHtml}
${discountSection}
</div>
${td.montantRecu > 0 ? `
<div class="payment">
<div class="row"><span>Montant reçu :</span><span>${td.montantRecu.toFixed(3)} TND</span></div>
<div class="row"><span>Monnaie rendue :</span><span>${td.monnaieRendre.toFixed(3)} TND</span></div>
</div>` : ''}
<div class="footer">
<div class="merci">MERCI DE VOTRE VISITE</div>
<div>www.mazego.fr</div>
<div>Imprimé le ${new Date().toLocaleString('fr-FR')}</div>
</div>
</div>
<script>window.onload=function(){setTimeout(function(){window.print();setTimeout(function(){window.close();},500);},250);};<\/script>
</body></html>`);
win.document.close();
}
/* ============================================================
PANIER MOBILE (tiroir)
============================================================ */
function openCartDrawer() {
document.getElementById('posCart').classList.add('mobile-open');
document.getElementById('cartBackdrop').classList.add('open');
}
function closeCartDrawer() {
document.getElementById('posCart').classList.remove('mobile-open');
document.getElementById('cartBackdrop').classList.remove('open');
}
document.getElementById('cartFab')?.addEventListener('click', openCartDrawer);
document.getElementById('cartCloseBtn')?.addEventListener('click', closeCartDrawer);
document.getElementById('cartBackdrop')?.addEventListener('click', closeCartDrawer);
/* ============================================================
MODALS UTILITAIRES
============================================================ */
function openModal(id) {
const el = document.getElementById(id);
if (el) el.classList.add('open');
}
function closeModal(id) {
const el = document.getElementById(id);
if (el) el.classList.remove('open');
}
// Fermer en cliquant sur le fond de l'overlay
document.querySelectorAll('.pos-modal-overlay').forEach(overlay => {
overlay.addEventListener('click', function (e) {
if (e.target === this) closeModal(this.id);
});
});
// Client badge → ouvre le modal de sélection
document.getElementById('customerBtn').addEventListener('click', function(e) {
e.stopPropagation();
openModal('customerModal');
});
// Bouton ajout client → ouvre le modal nouveau client
document.getElementById('addClientBtn')?.addEventListener('click', function(e) {
e.stopPropagation();
openModal('addClientModal');
});
// Sélection d'un client — listener direct sur chaque <li>
document.querySelectorAll('#customerModal .client-item').forEach(function(item) {
item.addEventListener('click', function(e) {
e.stopPropagation();
const id = this.dataset.clientId;
const name = this.dataset.clientName || this.querySelector('span').textContent.trim();
const initial = name.charAt(0).toUpperCase();
const f = { selectedClientId: id, selectedespeceClientId: id, selectedClientIdTicket: id };
Object.entries(f).forEach(([fid, val]) => { const el = document.getElementById(fid); if (el) el.value = val; });
const nd = document.getElementById('clientNameDisplay');
const ad = document.getElementById('clientAvatarText');
if (nd) nd.textContent = name;
if (ad) ad.textContent = initial;
closeModal('customerModal');
});
});
/* ============================================================
VIDER LE PANIER
============================================================ */
function confirmClearCart() {
if (confirm('Vider complètement le panier ? Cette action est irréversible.')) {
document.getElementById('clearCartForm').submit();
}
}
/* ============================================================
RECHERCHE & SCAN CODE-BARRES
============================================================ */
function isBarcodeLike(v) {
return /^\d{8,13}$/.test(v);
}
function scanBarcodeAndAddToCart(code) {
if (!CAISSE_SESSION_OUVERTE) {
openModal('noCaisseSessionModal');
return;
}
playCaisseBeep();
document.getElementById('scanBarcodeInput').value = code;
document.getElementById('scanBarcodeForm').submit();
}
document.addEventListener('DOMContentLoaded', function () {
const searchInput = document.getElementById('searchInput');
if (!searchInput) return;
searchInput.focus();
searchInput.addEventListener('click', function () { this.select(); });
searchInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
const v = this.value.trim();
if (isBarcodeLike(v)) {
scanBarcodeAndAddToCart(v);
} else {
this.form.submit();
}
}
});
let scanTimeout;
searchInput.addEventListener('input', function (e) {
const v = this.value.trim();
// Code-barres tapé/scanné : dès que la frappe s'arrête, on l'ajoute
// directement au panier (comportement d'un vrai scanner de caisse).
if (isBarcodeLike(v)) {
clearTimeout(scanTimeout);
scanTimeout = setTimeout(() => {
if (this.value.trim() === v) scanBarcodeAndAddToCart(v);
}, 150);
return;
}
// Certains scanners "collent" le code d'un coup (paste-like input) :
// même logique, mais on retombe sur la recherche classique si ce
// n'est finalement pas un code-barres (ex: texte collé).
if (e.inputType === 'insertReplacementText' || e.inputType === 'insertFromPaste') {
clearTimeout(scanTimeout);
scanTimeout = setTimeout(() => {
if (this.value.trim() !== v) return;
if (isBarcodeLike(v)) {
scanBarcodeAndAddToCart(v);
} else {
this.form.submit();
}
}, 50);
}
});
});
/* ============================================================
FORMULAIRES DE REMISE (AJAX)
============================================================ */
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.discount-form').forEach(form => {
form.addEventListener('submit', function (e) {
e.preventDefault();
fetch(this.action, {
method: 'POST',
body: new FormData(this),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.json())
.then(data => { if (data.success) window.location.reload(); })
.catch(() => this.submit());
});
});
});
/* ============================================================
CHANGEMENT DE SALARIÉ / DÉCONNEXION
============================================================ */
document.addEventListener('DOMContentLoaded', function () {
var logoutBtn = document.getElementById('shiftLogoutBtn');
if (!logoutBtn) return;
logoutBtn.addEventListener('click', function (e) {
var panierCount = parseInt(this.dataset.panierCount || '0', 10);
var message = panierCount > 0
? 'Le panier en cours contient ' + panierCount + ' article(s) non validé(s) qui seront perdus. Se déconnecter maintenant ?'
: 'Terminer votre service et laisser la place au salarié suivant ?';
if (!window.confirm(message)) {
e.preventDefault();
}
});
});
</script>
{% endblock %}