templates/caisse/point_vente/index.html.twig line 1

Open in your IDE?
  1. {% extends 'basecaisse.html.twig' %}
  2. {% block title %}Point de Vente{% endblock %}
  3. {% block stylesheets %}{% endblock %}
  4. {% block body %}
  5. <style>
  6. :root {
  7.     --pos-primary: #1a56db;
  8.     --pos-primary-dark: #1341b0;
  9.     --pos-success: #059669;
  10.     --pos-success-light: #d1fae5;
  11.     --pos-danger: #dc2626;
  12.     --pos-warning: #d97706;
  13.     --pos-warning-light: #fffbeb;
  14.     --pos-warning-border: #fcd34d;
  15.     --pos-gray-50: #f8fafc;
  16.     --pos-gray-100: #f1f5f9;
  17.     --pos-gray-200: #e2e8f0;
  18.     --pos-gray-400: #94a3b8;
  19.     --pos-gray-600: #475569;
  20.     --pos-gray-800: #1e293b;
  21.     --pos-radius: 10px;
  22.     --pos-shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.05);
  23.     --pos-shadow-md: 0 4px 12px rgba(0,0,0,.1);
  24.     --pos-header-grad: linear-gradient(120deg, #0b1230 0%, #101d4a 30%, #142a70 55%, #1a3fae 80%, #1a56db 100%);
  25.     --pos-header-border: rgba(96,165,250,.45);
  26.     --pos-header-h: 68px;
  27. }
  28. /* ===================== LAYOUT ===================== */
  29. .pos-wrapper {
  30.     display: grid;
  31.     grid-template-columns: 1fr clamp(420px, 33vw, 560px);
  32.     grid-template-rows: auto 1fr;
  33.     height: 100vh;
  34.     overflow: hidden;
  35.     background: var(--pos-gray-100);
  36.     gap: 0;
  37. }
  38. .pos-header {
  39.     grid-column: 1 / -1;
  40.     background: var(--pos-header-grad);
  41.     color: #fff;
  42.     display: flex;
  43.     align-items: center;
  44.     justify-content: space-between;
  45.     padding: 0 22px;
  46.     height: var(--pos-header-h);
  47.     border-bottom: 2px solid var(--pos-header-border);
  48.     box-shadow: 0 6px 18px rgba(2,6,23,.35);
  49.     z-index: 10;
  50.     flex-wrap: nowrap;
  51.     gap: 10px;
  52. }
  53. .pos-products {
  54.     overflow-y: auto;
  55.     display: flex;
  56.     flex-direction: column;
  57.     background: var(--pos-gray-100);
  58.     min-width: 0;
  59. }
  60. .pos-cart {
  61.     background: #fff;
  62.     border-left: 1px solid var(--pos-gray-200);
  63.     display: flex;
  64.     flex-direction: column;
  65.     overflow: hidden;
  66.     min-width: 0;
  67. }
  68. .cart-close-btn { display: none; }
  69. /* Bouton flottant panier (mobile uniquement) */
  70. .cart-fab {
  71.     display: none;
  72.     position: fixed;
  73.     right: 16px;
  74.     bottom: 16px;
  75.     z-index: 900;
  76.     background: var(--pos-success);
  77.     color: #fff;
  78.     border: none;
  79.     border-radius: 999px;
  80.     padding: 12px 18px;
  81.     box-shadow: 0 6px 18px rgba(0,0,0,.25);
  82.     font-size: 14px;
  83.     font-weight: 700;
  84.     align-items: center;
  85.     gap: 8px;
  86.     cursor: pointer;
  87. }
  88. .cart-fab .fab-count {
  89.     background: rgba(255,255,255,.25);
  90.     border-radius: 999px;
  91.     padding: 1px 8px;
  92.     font-size: 12px;
  93. }
  94. .cart-backdrop {
  95.     display: none;
  96.     position: fixed;
  97.     inset: 0;
  98.     background: rgba(15,23,42,.45);
  99.     z-index: 899;
  100. }
  101. /* ===================== HEADER ===================== */
  102. .header-client {
  103.     display: flex;
  104.     align-items: center;
  105.     gap: 10px;
  106. }
  107. .client-badge {
  108.     display: flex;
  109.     align-items: center;
  110.     gap: 10px;
  111.     min-height: 44px;
  112.     background: rgba(255,255,255,.12);
  113.     border: 1px solid rgba(255,255,255,.22);
  114.     border-radius: 26px;
  115.     padding: 6px 18px 6px 6px;
  116.     cursor: pointer;
  117.     transition: background .2s;
  118. }
  119. .client-badge:hover { background: rgba(255,255,255,.22); }
  120. .client-avatar {
  121.     width: 36px;
  122.     height: 36px;
  123.     border-radius: 50%;
  124.     background: rgba(255,255,255,.28);
  125.     display: flex;
  126.     align-items: center;
  127.     justify-content: center;
  128.     font-size: 14px;
  129.     font-weight: 700;
  130.     color: #fff;
  131.     flex-shrink: 0;
  132. }
  133. .client-name {
  134.     font-size: 15px;
  135.     font-weight: 700;
  136.     color: #fff;
  137.     background: none;
  138.     border: none;
  139.     cursor: pointer;
  140.     padding: 0;
  141. }
  142. .header-right {
  143.     display: flex;
  144.     align-items: center;
  145.     gap: 10px;
  146. }
  147. .total-display {
  148.     background: rgba(255,255,255,.14);
  149.     border: 1px solid rgba(255,255,255,.24);
  150.     border-radius: 12px;
  151.     padding: 9px 20px;
  152.     font-size: 21px;
  153.     font-weight: 800;
  154.     color: #fff;
  155.     letter-spacing: .3px;
  156.     min-height: 44px;
  157.     display: flex;
  158.     align-items: center;
  159. }
  160. /* ===================== USER SESSION / SHIFT CHANGE ===================== */
  161. .user-session-chip {
  162.     display: flex;
  163.     align-items: center;
  164.     gap: 10px;
  165.     min-height: 44px;
  166.     background: rgba(255,255,255,.12);
  167.     border: 1px solid rgba(255,255,255,.22);
  168.     border-radius: 26px;
  169.     padding: 5px 5px 5px 14px;
  170. }
  171. .user-session-avatar {
  172.     width: 34px;
  173.     height: 34px;
  174.     border-radius: 50%;
  175.     background: rgba(255,255,255,.28);
  176.     display: flex;
  177.     align-items: center;
  178.     justify-content: center;
  179.     font-size: 13px;
  180.     font-weight: 700;
  181.     color: #fff;
  182.     flex-shrink: 0;
  183. }
  184. .user-session-name {
  185.     font-size: 14px;
  186.     font-weight: 700;
  187.     color: #fff;
  188.     max-width: 150px;
  189.     overflow: hidden;
  190.     text-overflow: ellipsis;
  191.     white-space: nowrap;
  192. }
  193. .user-session-logout {
  194.     display: flex;
  195.     align-items: center;
  196.     justify-content: center;
  197.     width: 38px;
  198.     height: 38px;
  199.     border-radius: 50%;
  200.     background: rgba(220,38,38,.9);
  201.     color: #fff;
  202.     font-size: 14px;
  203.     text-decoration: none;
  204.     transition: background .15s;
  205.     flex-shrink: 0;
  206. }
  207. .user-session-logout:hover { background: rgba(185,28,28,.95); color: #fff; }
  208. /* ===================== BRAND / LOGO ===================== */
  209. .pos-brand {
  210.     display: flex;
  211.     align-items: center;
  212.     padding-left: 16px;
  213.     margin-left: 4px;
  214.     border-left: 1px solid rgba(255,255,255,.2);
  215.     user-select: none;
  216.     flex-shrink: 0;
  217. }
  218. .pos-brand-text {
  219.     display: inline-flex;
  220.     align-items: baseline;
  221.     font-size: 19px;
  222.     font-weight: 800;
  223.     letter-spacing: -.3px;
  224.     white-space: nowrap;
  225.     background: linear-gradient(100deg, #ffffff 0%, #ffffff 60%, #bfdbfe 100%);
  226.     -webkit-background-clip: text;
  227.     background-clip: text;
  228.     -webkit-text-fill-color: transparent;
  229.     text-shadow: 0 1px 12px rgba(96,165,250,.35);
  230. }
  231. .pos-brand-tld {
  232.     font-weight: 600;
  233.     color: #7dabfa;
  234.     -webkit-text-fill-color: #7dabfa;
  235. }
  236. @media (max-width: 640px) {
  237.     .user-session-name { display: none; }
  238.     .user-session-chip { padding: 5px; gap: 6px; }
  239.     .pos-brand { padding-left: 10px; }
  240.     .pos-brand-text { font-size: 16px; }
  241. }
  242. @media (max-width: 420px) {
  243.     .pos-brand { display: none; }
  244. }
  245. .btn-dashboard {
  246.     background: rgba(255,255,255,.14);
  247.     border: 1px solid rgba(255,255,255,.24);
  248.     border-radius: 12px;
  249.     padding: 10px 18px;
  250.     min-height: 44px;
  251.     font-size: 14px;
  252.     font-weight: 600;
  253.     color: #fff;
  254.     text-decoration: none;
  255.     transition: background .15s, transform .1s;
  256.     display: flex;
  257.     align-items: center;
  258.     gap: 9px;
  259. }
  260. .btn-dashboard:hover {
  261.     background: rgba(255,255,255,.26);
  262.     color: #fff;
  263.     text-decoration: none;
  264. }
  265. .btn-dashboard:active { transform: scale(.97); }
  266. /* ===================== CATEGORIES ===================== */
  267. .category-bar {
  268.     background: #fff;
  269.     border-bottom: 1px solid var(--pos-gray-200);
  270.     padding: 10px 16px;
  271.     display: flex;
  272.     gap: 8px;
  273.     overflow-x: auto;
  274.     flex-shrink: 0;
  275. }
  276. .category-bar::-webkit-scrollbar { height: 3px; }
  277. .category-bar::-webkit-scrollbar-thumb { background: var(--pos-gray-200); border-radius: 3px; }
  278. .cat-pill {
  279.     display: inline-flex;
  280.     align-items: center;
  281.     min-height: 44px;
  282.     padding: 10px 20px;
  283.     border-radius: 22px;
  284.     border: 1px solid var(--pos-gray-200);
  285.     background: var(--pos-gray-50);
  286.     font-size: 14.5px;
  287.     font-weight: 600;
  288.     color: var(--pos-gray-600);
  289.     text-decoration: none;
  290.     white-space: nowrap;
  291.     transition: all .15s;
  292.     flex-shrink: 0;
  293. }
  294. .cat-pill:active { transform: scale(.96); }
  295. .cat-pill:hover {
  296.     background: var(--pos-gray-100);
  297.     border-color: var(--pos-gray-400);
  298.     color: var(--pos-gray-800);
  299.     text-decoration: none;
  300. }
  301. .cat-pill.active {
  302.     background: var(--pos-primary);
  303.     border-color: var(--pos-primary);
  304.     color: #fff;
  305. }
  306. /* ===================== SEARCH ===================== */
  307. .search-bar {
  308.     padding: 10px 16px;
  309.     background: #fff;
  310.     border-bottom: 1px solid var(--pos-gray-200);
  311.     flex-shrink: 0;
  312. }
  313. .search-inner {
  314.     position: relative;
  315. }
  316. .search-inner i {
  317.     position: absolute;
  318.     left: 12px;
  319.     top: 50%;
  320.     transform: translateY(-50%);
  321.     color: var(--pos-gray-400);
  322.     font-size: 13px;
  323. }
  324. .search-input {
  325.     width: 100%;
  326.     padding: 8px 12px 8px 36px;
  327.     border: 1px solid var(--pos-gray-200);
  328.     border-radius: 8px;
  329.     font-size: 13px;
  330.     background: var(--pos-gray-50);
  331.     color: var(--pos-gray-800);
  332.     outline: none;
  333.     transition: border-color .18s, box-shadow .18s;
  334. }
  335. .search-input:focus {
  336.     border-color: var(--pos-primary);
  337.     box-shadow: 0 0 0 3px rgba(26,86,219,.1);
  338.     background: #fff;
  339. }
  340. /* ===================== PRODUCT GRID ===================== */
  341. .products-scroll {
  342.     flex: 1;
  343.     overflow-y: auto;
  344.     padding: 16px;
  345. }
  346. .product-grid {
  347.     display: grid;
  348.     grid-template-columns: repeat(auto-fill, minmax(155px, 1fr));
  349.     gap: 12px;
  350. }
  351. .product-card {
  352.     background: #fff;
  353.     border-radius: var(--pos-radius);
  354.     overflow: hidden;
  355.     cursor: pointer;
  356.     border: 1px solid var(--pos-gray-200);
  357.     transition: transform .18s, box-shadow .18s, border-color .18s;
  358.     position: relative;
  359. }
  360. .product-card:hover {
  361.     transform: translateY(-3px);
  362.     box-shadow: var(--pos-shadow-md);
  363.     border-color: var(--pos-primary);
  364. }
  365. .product-card:active { transform: scale(.97); }
  366. .product-img {
  367.     height: 98px;
  368.     background-size: cover;
  369.     background-position: center;
  370.     background-color: var(--pos-gray-100);
  371.     position: relative;
  372. }
  373. .product-body {
  374.     padding: 10px;
  375. }
  376. .product-name {
  377.     font-size: 12px;
  378.     font-weight: 600;
  379.     color: var(--pos-gray-800);
  380.     line-height: 1.35;
  381.     margin-bottom: 6px;
  382.     display: -webkit-box;
  383.     -webkit-line-clamp: 2;
  384.     -webkit-box-orient: vertical;
  385.     overflow: hidden;
  386. }
  387. .product-price {
  388.     font-size: 14px;
  389.     font-weight: 700;
  390.     color: var(--pos-primary);
  391. }
  392. .product-price-buy {
  393.     font-size: 10px;
  394.     color: var(--pos-gray-400);
  395.     margin-top: 2px;
  396. }
  397. .discount-badge {
  398.     position: absolute;
  399.     top: 8px;
  400.     right: 8px;
  401.     background: #ef4444;
  402.     color: #fff;
  403.     font-size: 10px;
  404.     font-weight: 700;
  405.     padding: 2px 6px;
  406.     border-radius: 10px;
  407. }
  408. .no-products {
  409.     grid-column: 1 / -1;
  410.     text-align: center;
  411.     padding: 60px 20px;
  412.     color: var(--pos-gray-400);
  413.     font-size: 14px;
  414. }
  415. /* ===================== CART ===================== */
  416. .cart-header {
  417.     padding: 14px 16px;
  418.     background: var(--pos-primary);
  419.     color: #fff;
  420.     font-size: 14px;
  421.     font-weight: 600;
  422.     display: flex;
  423.     align-items: center;
  424.     gap: 8px;
  425.     flex-shrink: 0;
  426. }
  427. .cart-body {
  428.     flex: 1;
  429.     min-height: 120px;
  430.     overflow-y: auto;
  431.     padding: 10px;
  432.     background: var(--pos-gray-50);
  433. }
  434. .cart-empty {
  435.     display: flex;
  436.     flex-direction: column;
  437.     align-items: center;
  438.     justify-content: center;
  439.     height: 100%;
  440.     color: var(--pos-gray-400);
  441.     gap: 10px;
  442.     font-size: 13px;
  443. }
  444. .cart-item {
  445.     display: block; /* basecaisse.html.twig impose display:flex (ancien panier) */
  446.     background: #fff;
  447.     border-radius: 8px;
  448.     padding: 10px 12px;
  449.     margin-bottom: 8px;
  450.     border: 1px solid var(--pos-gray-200);
  451.     border-left: 3px solid var(--pos-primary);
  452.     transition: border-color .18s;
  453. }
  454. .cart-item.has-discount { border-left-color: var(--pos-warning); }
  455. .cart-item-top {
  456.     display: flex;
  457.     justify-content: space-between;
  458.     align-items: flex-start;
  459.     gap: 8px;
  460. }
  461. .cart-item-name {
  462.     font-size: 12px;
  463.     font-weight: 600;
  464.     color: var(--pos-gray-800);
  465.     line-height: 1.3;
  466.     flex: 1;
  467.     min-width: 0;
  468. }
  469. .cart-item-total {
  470.     font-size: 13px;
  471.     font-weight: 700;
  472.     color: var(--pos-primary);
  473.     white-space: nowrap;
  474.     flex-shrink: 0;
  475. }
  476. .cart-item-meta {
  477.     font-size: 11px;
  478.     color: var(--pos-gray-400);
  479.     margin: 3px 0 8px;
  480. }
  481. .cart-item-controls {
  482.     display: flex;
  483.     align-items: center;
  484.     gap: 6px;
  485. }
  486. .qty-btn {
  487.     width: 38px;
  488.     height: 38px;
  489.     border-radius: 9px;
  490.     border: 1px solid var(--pos-gray-200);
  491.     background: var(--pos-gray-50);
  492.     color: var(--pos-gray-600);
  493.     font-size: 18px;
  494.     font-weight: 700;
  495.     cursor: pointer;
  496.     display: flex;
  497.     align-items: center;
  498.     justify-content: center;
  499.     transition: all .15s;
  500.     padding: 0;
  501. }
  502. .qty-btn:hover { background: var(--pos-gray-200); }
  503. .qty-btn:active { transform: scale(.94); }
  504. .qty-display {
  505.     font-size: 16px;
  506.     font-weight: 700;
  507.     color: var(--pos-gray-800);
  508.     min-width: 28px;
  509.     text-align: center;
  510. }
  511. .btn-remove {
  512.     margin-left: auto;
  513.     width: 38px;
  514.     height: 38px;
  515.     border-radius: 9px;
  516.     border: 1px solid #fecaca;
  517.     background: #fef2f2;
  518.     color: var(--pos-danger);
  519.     cursor: pointer;
  520.     display: flex;
  521.     align-items: center;
  522.     justify-content: center;
  523.     font-size: 14px;
  524.     transition: all .15s;
  525.     padding: 0;
  526. }
  527. .btn-remove:hover { background: #fee2e2; }
  528. .btn-remove:active { transform: scale(.94); }
  529. .discount-row {
  530.     display: flex;
  531.     align-items: center;
  532.     gap: 6px;
  533.     margin-top: 8px;
  534. }
  535. .discount-row i.fa-tag {
  536.     flex-shrink: 0;
  537. }
  538. .discount-input-small {
  539.     flex: 1 1 44px;
  540.     min-width: 36px;
  541.     width: auto;
  542.     height: 36px;
  543.     border: 1px solid var(--pos-gray-200);
  544.     border-radius: 8px;
  545.     font-size: 13px;
  546.     text-align: right;
  547.     padding: 0 8px;
  548.     color: var(--pos-gray-800);
  549.     background: var(--pos-gray-50);
  550.     outline: none;
  551. }
  552. .discount-input-small:focus {
  553.     border-color: var(--pos-warning);
  554.     background: #fff;
  555. }
  556. .discount-submit-btn {
  557.     flex-shrink: 0;
  558.     height: 36px;
  559.     padding: 0 14px;
  560.     border: 1px solid var(--pos-warning);
  561.     background: var(--pos-warning-light);
  562.     color: var(--pos-warning);
  563.     border-radius: 8px;
  564.     font-size: 12.5px;
  565.     font-weight: 600;
  566.     cursor: pointer;
  567.     white-space: nowrap;
  568.     transition: all .15s;
  569. }
  570. .discount-submit-btn:hover { background: #fef3c7; }
  571. /* ===================== CART SUMMARY ===================== */
  572. .cart-footer {
  573.     flex-shrink: 0;
  574.     border-top: 1px solid var(--pos-gray-200);
  575.     background: #fff;
  576.     /* Les articles gardent toujours de la place, même avec les boutons restaurant */
  577.     max-height: 58%;
  578.     overflow-y: auto;
  579. }
  580. .cart-summary {
  581.     padding: 12px 16px 0;
  582. }
  583. .summary-row {
  584.     display: flex;
  585.     justify-content: space-between;
  586.     align-items: center;
  587.     font-size: 12px;
  588.     color: var(--pos-gray-600);
  589.     padding: 3px 0;
  590. }
  591. .summary-row.ttc-row {
  592.     font-size: 15px;
  593.     font-weight: 700;
  594.     color: var(--pos-gray-800);
  595.     background: #eff6ff;
  596.     border-radius: 8px;
  597.     padding: 8px 10px;
  598.     margin: 6px 0 0;
  599. }
  600. /* Remise globale */
  601. .global-discount-block {
  602.     margin: 10px 0 4px;
  603.     background: #fffbeb;
  604.     border: 1.5px solid #f59e0b;
  605.     border-radius: 10px;
  606.     padding: 10px 14px;
  607.     transition: box-shadow .2s;
  608. }
  609. .global-discount-block:focus-within {
  610.     box-shadow: 0 0 0 3px rgba(245,158,11,.15);
  611. }
  612. .global-discount-label {
  613.     font-size: 11px;
  614.     font-weight: 700;
  615.     color: #92400e;
  616.     margin-bottom: 8px;
  617.     display: flex;
  618.     align-items: center;
  619.     gap: 5px;
  620.     text-transform: uppercase;
  621.     letter-spacing: .4px;
  622. }
  623. .global-discount-inputs {
  624.     display: flex;
  625.     align-items: center;
  626.     gap: 8px;
  627. }
  628. .global-discount-input {
  629.     flex: 1;
  630.     min-width: 0;
  631.     height: 36px;
  632.     border: 1.5px solid #f59e0b;
  633.     border-radius: 8px;
  634.     font-size: 15px;
  635.     font-weight: 700;
  636.     text-align: right;
  637.     padding: 0 10px;
  638.     background: #fff;
  639.     color: #1e293b;
  640.     outline: none;
  641.     transition: border-color .18s, box-shadow .18s;
  642. }
  643. .global-discount-input:focus {
  644.     border-color: #d97706;
  645.     box-shadow: 0 0 0 2px rgba(217,119,6,.2);
  646. }
  647. .global-discount-input::placeholder { color: #d1d5db; font-weight: 400; }
  648. .global-discount-unit {
  649.     font-size: 13px;
  650.     color: #92400e;
  651.     font-weight: 700;
  652.     white-space: nowrap;
  653. }
  654. .global-discount-amount {
  655.     font-size: 12px;
  656.     font-weight: 700;
  657.     color: #dc2626;
  658.     margin-left: auto;
  659.     white-space: nowrap;
  660. }
  661. /* Total net */
  662. .total-net-row {
  663.     display: flex;
  664.     justify-content: space-between;
  665.     align-items: center;
  666.     background: #ecfdf5;
  667.     border: 1.5px solid #10b981;
  668.     border-radius: 10px;
  669.     padding: 10px 14px;
  670.     margin: 6px 0 0;
  671.     animation: fadeSlideIn .2s ease;
  672. }
  673. @keyframes fadeSlideIn {
  674.     from { opacity: 0; transform: translateY(-4px); }
  675.     to   { opacity: 1; transform: translateY(0); }
  676. }
  677. .total-net-label {
  678.     font-size: 12px;
  679.     font-weight: 700;
  680.     color: #065f46;
  681.     text-transform: uppercase;
  682.     letter-spacing: .4px;
  683. }
  684. .total-net-value {
  685.     font-size: 20px;
  686.     font-weight: 800;
  687.     color: #065f46;
  688. }
  689. /* ===================== ACTION BUTTONS ===================== */
  690. .cart-actions {
  691.     padding: 12px 16px 16px;
  692. }
  693. .btn-primary-action {
  694.     width: 100%;
  695.     min-height: 54px;
  696.     padding: 15px;
  697.     background: var(--pos-success);
  698.     color: #fff;
  699.     border: none;
  700.     border-radius: var(--pos-radius);
  701.     font-size: 16.5px;
  702.     font-weight: 700;
  703.     cursor: pointer;
  704.     display: flex;
  705.     align-items: center;
  706.     justify-content: center;
  707.     gap: 9px;
  708.     transition: background .18s, transform .12s;
  709.     margin-bottom: 10px;
  710. }
  711. .btn-primary-action:hover { background: #047857; }
  712. .btn-primary-action:active { transform: scale(.98); }
  713. .action-row {
  714.     display: grid;
  715.     grid-template-columns: 1fr 1fr;
  716.     gap: 8px;
  717.     margin-bottom: 8px;
  718. }
  719. .btn-action {
  720.     min-height: 48px;
  721.     padding: 12px 8px;
  722.     border: 1px solid var(--pos-gray-200);
  723.     border-radius: 10px;
  724.     font-size: 13.5px;
  725.     font-weight: 600;
  726.     cursor: pointer;
  727.     display: flex;
  728.     align-items: center;
  729.     justify-content: center;
  730.     gap: 7px;
  731.     transition: all .15s;
  732.     background: #fff;
  733.     color: var(--pos-gray-600);
  734. }
  735. .btn-action:active { transform: scale(.96); }
  736. .btn-action:hover { background: var(--pos-gray-50); border-color: var(--pos-gray-400); }
  737. .btn-action.cash   { border-color: #a7f3d0; background: #ecfdf5; color: #047857; }
  738. .btn-action.cash:hover { background: #d1fae5; }
  739. .btn-action.unpaid { border-color: #fcd34d; background: var(--pos-warning-light); color: var(--pos-warning); }
  740. .btn-action.unpaid:hover { background: #fef3c7; }
  741. .btn-action.danger { border-color: #fecaca; background: #fef2f2; color: var(--pos-danger); }
  742. .btn-action.danger:hover { background: #fee2e2; }
  743. .btn-action.client { border-color: #bfdbfe; background: #eff6ff; color: var(--pos-primary); }
  744. .btn-action.client:hover { background: #dbeafe; }
  745. .btn-action.add-prod { border-color: #c7d2fe; background: #eef2ff; color: #4338ca; }
  746. .btn-action.add-prod:hover { background: #e0e7ff; }
  747. .btn-action.add-cat { border-color: var(--pos-gray-200); background: var(--pos-gray-50); color: var(--pos-gray-600); }
  748. /* ===================== MODALS ===================== */
  749. .pos-modal-overlay {
  750.     display: none;
  751.     position: fixed;
  752.     inset: 0;
  753.     background: rgba(15,23,42,.45);
  754.     z-index: 1000;
  755.     align-items: center;
  756.     justify-content: center;
  757. }
  758. .pos-modal-overlay.open { display: flex !important; }
  759. .pos-modal {
  760.     background: #fff;
  761.     border-radius: 14px;
  762.     width: 100%;
  763.     max-width: 440px;
  764.     max-height: 90vh;
  765.     margin: 20px;
  766.     box-shadow: 0 20px 60px rgba(0,0,0,.2);
  767.     overflow: hidden;
  768.     display: flex;
  769.     flex-direction: column;
  770. }
  771. .pos-modal-header {
  772.     background: var(--pos-primary);
  773.     color: #fff;
  774.     padding: 16px 20px;
  775.     font-size: 16px;
  776.     font-weight: 600;
  777.     display: flex;
  778.     align-items: center;
  779.     justify-content: space-between;
  780.     flex-shrink: 0;
  781. }
  782. .pos-modal-close {
  783.     background: rgba(255,255,255,.15);
  784.     border: none;
  785.     color: #fff;
  786.     width: 40px;
  787.     height: 40px;
  788.     border-radius: 10px;
  789.     cursor: pointer;
  790.     font-size: 20px;
  791.     display: flex;
  792.     align-items: center;
  793.     justify-content: center;
  794.     flex-shrink: 0;
  795. }
  796. .pos-modal-body { padding: 20px; overflow-y: auto; flex: 1; min-height: 0; }
  797. /* Cash modal */
  798. .cash-amount-box {
  799.     background: var(--pos-gray-50);
  800.     border: 1px solid var(--pos-gray-200);
  801.     border-radius: 10px;
  802.     padding: 14px 16px;
  803.     margin-bottom: 12px;
  804.     text-align: center;
  805. }
  806. .cash-amount-box.highlight {
  807.     background: #fffbeb;
  808.     border-color: var(--pos-warning-border);
  809. }
  810. .cash-label {
  811.     font-size: 12px;
  812.     color: var(--pos-gray-400);
  813.     font-weight: 500;
  814.     display: block;
  815.     margin-bottom: 6px;
  816. }
  817. .cash-value {
  818.     font-size: 26px;
  819.     font-weight: 800;
  820.     color: var(--pos-gray-800);
  821. }
  822. .cash-input {
  823.     width: 100%;
  824.     border: 2px solid var(--pos-gray-200);
  825.     border-radius: 8px;
  826.     padding: 10px;
  827.     font-size: 26px;
  828.     font-weight: 800;
  829.     text-align: center;
  830.     color: var(--pos-gray-800);
  831.     outline: none;
  832.     transition: border-color .18s;
  833.     background: #fff;
  834. }
  835. .cash-input:focus { border-color: var(--pos-primary); }
  836. .exact-amount-btn {
  837.     width: 100%;
  838.     min-height: 48px;
  839.     padding: 12px;
  840.     margin-top: 10px;
  841.     background: var(--pos-success-light);
  842.     border: 1.5px solid var(--pos-success);
  843.     border-radius: 10px;
  844.     font-size: 15px;
  845.     font-weight: 700;
  846.     color: var(--pos-success);
  847.     cursor: pointer;
  848.     display: flex;
  849.     align-items: center;
  850.     justify-content: center;
  851.     gap: 9px;
  852.     transition: all .15s;
  853. }
  854. .exact-amount-btn:hover { background: #a7f3d0; }
  855. .exact-amount-btn:active { transform: scale(.97); }
  856. .quick-btns {
  857.     display: flex;
  858.     flex-wrap: wrap;
  859.     gap: 8px;
  860.     justify-content: center;
  861.     margin: 14px 0;
  862. }
  863. .quick-btn {
  864.     min-height: 44px;
  865.     min-width: 56px;
  866.     padding: 10px 16px;
  867.     background: #eff6ff;
  868.     border: 1px solid #bfdbfe;
  869.     border-radius: 10px;
  870.     font-size: 15px;
  871.     font-weight: 700;
  872.     color: var(--pos-primary);
  873.     cursor: pointer;
  874.     transition: all .15s;
  875. }
  876. .quick-btn:hover { background: #dbeafe; }
  877. .quick-btn:active { transform: scale(.95); }
  878. .modal-action-row {
  879.     display: flex;
  880.     gap: 10px;
  881.     margin-top: 16px;
  882. }
  883. .btn-modal-cancel {
  884.     flex: 1;
  885.     min-height: 48px;
  886.     padding: 13px;
  887.     background: var(--pos-gray-100);
  888.     border: 1px solid var(--pos-gray-200);
  889.     border-radius: 10px;
  890.     font-size: 14.5px;
  891.     font-weight: 600;
  892.     color: var(--pos-gray-600);
  893.     cursor: pointer;
  894.     transition: all .15s;
  895. }
  896. .btn-modal-cancel:hover { background: var(--pos-gray-200); }
  897. .btn-modal-confirm {
  898.     flex: 2;
  899.     min-height: 48px;
  900.     padding: 13px;
  901.     background: var(--pos-success);
  902.     border: none;
  903.     border-radius: 10px;
  904.     font-size: 15.5px;
  905.     font-weight: 700;
  906.     color: #fff;
  907.     cursor: pointer;
  908.     transition: background .15s;
  909. }
  910. .btn-modal-confirm:hover { background: #047857; }
  911. /* Client modal */
  912. .client-list {
  913.     list-style: none;
  914.     padding: 0;
  915.     margin: 0;
  916.     max-height: 55vh;
  917.     overflow-y: auto;
  918. }
  919. .client-item {
  920.     min-height: 48px;
  921.     padding: 14px 16px;
  922.     border-bottom: 1px solid var(--pos-gray-100);
  923.     cursor: pointer;
  924.     font-size: 14.5px;
  925.     color: var(--pos-gray-800);
  926.     display: flex;
  927.     justify-content: space-between;
  928.     align-items: center;
  929.     transition: background .15s;
  930. }
  931. .client-item:hover { background: var(--pos-gray-50); }
  932. .client-item::after { content: "›"; color: var(--pos-gray-400); font-size: 18px; }
  933. /* Scrollbars */
  934. .pos-products::-webkit-scrollbar,
  935. .cart-body::-webkit-scrollbar,
  936. .products-scroll::-webkit-scrollbar { width: 5px; }
  937. .pos-products::-webkit-scrollbar-thumb,
  938. .cart-body::-webkit-scrollbar-thumb,
  939. .products-scroll::-webkit-scrollbar-thumb { background: var(--pos-gray-200); border-radius: 3px; }
  940. /* ===================== RESPONSIVE ===================== */
  941. /* Grands ecrans : un peu plus d'air pour le panier */
  942. @media (min-width: 1400px) {
  943.     .pos-wrapper { grid-template-columns: 1fr clamp(460px, 32vw, 600px); }
  944. }
  945. /* Ecrans moyens / petits laptops */
  946. @media (max-width: 1199px) {
  947.     .pos-wrapper { grid-template-columns: 1fr clamp(380px, 36vw, 460px); }
  948.     .product-grid { grid-template-columns: repeat(auto-fill, minmax(135px, 1fr)); }
  949. }
  950. /* Tablettes en mode paysage */
  951. @media (max-width: 991px) {
  952.     .pos-wrapper { grid-template-columns: 1fr clamp(320px, 40vw, 380px); }
  953.     .product-grid { grid-template-columns: repeat(auto-fill, minmax(115px, 1fr)); }
  954. }
  955. /* Tablettes portrait & mobiles : panier en tiroir plein ecran */
  956. @media (max-width: 767px) {
  957.     .pos-wrapper { grid-template-columns: 1fr; }
  958.     .pos-products { grid-column: 1 / -1; }
  959.     .product-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
  960.     .pos-cart {
  961.         position: fixed;
  962.         top: 0;
  963.         right: 0;
  964.         bottom: 0;
  965.         width: min(88vw, 380px);
  966.         border-left: none;
  967.         box-shadow: -8px 0 24px rgba(0,0,0,.2);
  968.         transform: translateX(100%);
  969.         transition: transform .25s ease;
  970.         z-index: 950;
  971.     }
  972.     .pos-cart.mobile-open { transform: translateX(0); }
  973.     .cart-close-btn {
  974.         display: flex;
  975.         align-items: center;
  976.         justify-content: center;
  977.         width: 40px;
  978.         height: 40px;
  979.         margin-left: auto;
  980.         background: rgba(255,255,255,.15);
  981.         border: none;
  982.         border-radius: 10px;
  983.         color: #fff;
  984.         font-size: 16px;
  985.         cursor: pointer;
  986.         flex-shrink: 0;
  987.     }
  988.     .cart-fab { display: flex; }
  989.     .cart-backdrop.open { display: block; }
  990.     /* Header : on laisse les boutons passer à la ligne plutôt que de les
  991.        couper hors écran — aucun élément de navigation ne doit devenir
  992.        inaccessible faute de place. */
  993.     .pos-header {
  994.         --pos-header-h: 60px;
  995.         flex-wrap: wrap;
  996.         height: auto;
  997.         min-height: var(--pos-header-h);
  998.         padding: 10px 14px;
  999.         row-gap: 8px;
  1000.     }
  1001.     .header-client { flex: 0 1 auto; }
  1002.     .header-right {
  1003.         flex: 1 1 100%;
  1004.         flex-wrap: wrap;
  1005.         justify-content: flex-end;
  1006.         row-gap: 8px;
  1007.     }
  1008.     .btn-dashboard .dashboard-label { display: none; }
  1009.     .btn-dashboard { padding: 10px; min-width: 44px; justify-content: center; }
  1010.     .total-display { font-size: 16px; padding: 7px 14px; min-height: 40px; }
  1011.     /* Grille produits : cartes plus petites pour en montrer davantage,
  1012.        et marge basse suffisante pour ne jamais passer sous le bouton
  1013.        panier flottant. */
  1014.     .products-scroll { padding: 12px; padding-bottom: 104px; }
  1015.     .product-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 10px; }
  1016.     .product-img { height: 76px; }
  1017.     .product-body { padding: 8px; }
  1018.     .product-name { font-size: 11px; margin-bottom: 4px; }
  1019.     .product-price { font-size: 12.5px; }
  1020.     .product-price-buy { font-size: 9px; }
  1021.     /* Barre de catégories : navigation tactile fluide, aucune pastille
  1022.        tronquée en bord d'écran. */
  1023.     .category-bar { padding: 8px 12px; gap: 6px; scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch; }
  1024.     .category-bar .cat-pill { scroll-snap-align: start; }
  1025.     .category-bar .cat-pill:last-child { margin-right: 4px; }
  1026. }
  1027. @media (max-width: 420px) {
  1028.     .product-grid { grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 8px; }
  1029.     .product-img { height: 64px; }
  1030.     .client-name { max-width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  1031. }
  1032. /* ===== Contexte de commande (attente, tables, restaurant) ===== */
  1033. .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); }
  1034. .order-context-links { display:flex; gap:6px; }
  1035. .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; }
  1036. .ctx-btn:hover { border-color:var(--pos-primary); color:var(--pos-primary); }
  1037. .ctx-btn.ctx-tables { border-color:#bfdbfe; background:#eff6ff; color:var(--pos-primary); }
  1038. .ctx-count { background:var(--pos-warning); color:#fff; border-radius:10px; padding:0 7px; font-size:11px; }
  1039. .order-context-form { display:flex; gap:6px; align-items:center; margin:0; }
  1040. .ctx-select { border:1px solid var(--pos-gray-200); border-radius:8px; padding:5px 8px; font-size:12.5px; background:#fff; }
  1041. .ctx-info { font-size:12.5px; color:var(--pos-gray-600); font-weight:600; }
  1042. .ctx-couverts { display:inline-flex; align-items:center; gap:4px; margin:0; font-size:12px; color:var(--pos-gray-600); }
  1043. .ctx-couverts input { width:44px; border:1px solid var(--pos-gray-200); border-radius:8px; padding:4px 6px; font-size:12.5px; }
  1044. .line-options { margin-top:6px; }
  1045. .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; }
  1046. .line-options summary::-webkit-details-marker { display:none; }
  1047. .line-options summary::before { content:'▸'; font-size:10px; transition:transform .15s; }
  1048. .line-options[open] summary::before { transform:rotate(90deg); }
  1049. .cart-item-note { font-size:11.5px; color:#7c3aed; margin-top:3px; }
  1050. .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; }
  1051. .note-input { width:100%; text-align:left; }
  1052. .btn-action.card { border-color:#c7d2fe; background:#eef2ff; color:#3730a3; }
  1053. .btn-action.card:hover { background:#e0e7ff; }
  1054. .btn-action.kitchen { border-color:#fed7aa; background:#fff7ed; color:#c2410c; }
  1055. .btn-action.kitchen.pending { box-shadow:0 0 0 2px #fb923c inset; }
  1056. .btn-action.hold { border-color:#ddd6fe; background:#f5f3ff; color:#6d28d9; }
  1057. .hold-form { display:flex; gap:6px; margin:0 0 8px; }
  1058. .btn-action.kitchen, .btn-action.hold, .order-context + .cart-body ~ .cart-footer .btn-action.client { min-height:42px; padding:9px 8px; }
  1059. .hold-input { flex:1; min-width:0; border:1px solid var(--pos-gray-200); border-radius:8px; padding:6px 8px; font-size:12.5px; }
  1060. .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; }
  1061. </style>
  1062. {# ========== HIDDEN CLIENT ID ========== #}
  1063. <input type="hidden" id="selectedClientId" value="">
  1064. {% include 'caisse/_flashes.html.twig' %}
  1065. <div class="pos-wrapper">
  1066.     {# ========== HEADER ========== #}
  1067.     <header class="pos-header">
  1068.         <div class="header-client">
  1069.             <div class="client-badge" id="customerBtn">
  1070.                 <div class="client-avatar" id="clientAvatarText">C</div>
  1071.                 <span class="client-name" id="clientNameDisplay">Client Comptant</span>
  1072.             </div>
  1073.         </div>
  1074.         <div class="header-right">
  1075.             {% if sessionCaisse %}
  1076.                 <span class="btn-dashboard" style="background:rgba(255,255,255,.14);cursor:default;" title="Caisse ouverte par {{ sessionCaisse.ouvertParNom }}">
  1077.                     <i class="fas fa-lock-open" style="font-size:15px;"></i>
  1078.                     <span class="dashboard-label">Caisse ouverte depuis {{ sessionCaisse.dateOuverture|date('H:i') }} — fond {{ sessionCaisse.fondOuverture|number_format(3, '.', '') }}</span>
  1079.                 </span>
  1080.                 {% if canCloture %}
  1081.                     <a href="{{ path('caisse_cloture_fermer', {'id': sessionCaisse.id}) }}" class="btn-dashboard" style="background:#dc2626;border-color:rgba(255,255,255,.3);">
  1082.                         <i class="fas fa-lock" style="font-size:15px;"></i>
  1083.                         <span class="dashboard-label">Clôturer la caisse</span>
  1084.                     </a>
  1085.                 {% endif %}
  1086.             {% else %}
  1087.                 <a href="{{ path('caisse_cloture_ouvrir') }}" class="btn-dashboard" style="background:#059669;border-color:rgba(255,255,255,.3);">
  1088.                     <i class="fas fa-unlock" style="font-size:15px;"></i>
  1089.                     <span class="dashboard-label">Ouvrir la caisse</span>
  1090.                 </a>
  1091.             {% endif %}
  1092.             {% if canVoirRapports %}
  1093.                 <a href="{{ path('caisse_rapports_vendeurs') }}" class="btn-dashboard">
  1094.                     <i class="fas fa-chart-column" style="font-size:15px;"></i>
  1095.                     <span class="dashboard-label">Mes ventes</span>
  1096.                 </a>
  1097.             {% endif %}
  1098.             {% if canReparations %}
  1099.                 <a href="{{ path('reparation_index') }}" class="btn-dashboard">
  1100.                     <i class="fas fa-screwdriver-wrench" style="font-size:15px;"></i>
  1101.                     <span class="dashboard-label">Réparations</span>
  1102.                 </a>
  1103.             {% endif %}
  1104.             {% if not isSalarie %}
  1105.                 <a href="{{ path('dashboard') }}" class="btn-dashboard">
  1106.                     <i class="fas fa-tachometer-alt" style="font-size:15px;"></i>
  1107.                     <span class="dashboard-label">Tableau de bord</span>
  1108.                 </a>
  1109.             {% endif %}
  1110.             <div class="total-display" id="totalPrice">{{ totalTTC|number_format(3, '.', '') }} TND</div>
  1111.             {% if principalNom %}
  1112.                 <div class="user-session-chip" title="{{ isSalarie ? 'Salarié connecté' : 'Compte connecté' }}">
  1113.                     <div class="user-session-avatar">{{ principalNom|slice(0, 1)|upper }}</div>
  1114.                     <span class="user-session-name">{{ principalNom }}</span>
  1115.                     <a href="{{ path('app_logout') }}" class="user-session-logout" id="shiftLogoutBtn"
  1116.                        data-panier-count="{{ panier|length }}"
  1117.                        title="{{ isSalarie ? 'Terminer mon service / changer de salarié' : 'Se déconnecter' }}">
  1118.                         <i class="fas fa-right-from-bracket"></i>
  1119.                     </a>
  1120.                 </div>
  1121.             {% endif %}
  1122.             <div class="pos-brand" title="Mazego.fr — Logiciel de caisse">
  1123.                 <span class="pos-brand-text">Mazego<span class="pos-brand-tld">.fr</span></span>
  1124.             </div>
  1125.         </div>
  1126.     </header>
  1127.     {# ========== PRODUCTS SECTION ========== #}
  1128.     <div class="pos-products">
  1129.         {# Categories #}
  1130.         <div class="category-bar">
  1131.             <a href="{{ path('point_vente_index') }}" class="cat-pill {{ selectedCategory is null ? 'active' : '' }}">
  1132.                 Tous
  1133.             </a>
  1134.             {% for categorie in categories %}
  1135.                 <a href="{{ path('point_vente_index', {'categorie': categorie.id}) }}"
  1136.                    class="cat-pill {{ selectedCategory == categorie.id ? 'active' : '' }}">
  1137.                     {{ categorie.nom }}
  1138.                 </a>
  1139.             {% endfor %}
  1140.         </div>
  1141.         {# Search #}
  1142.         <div class="search-bar">
  1143.             <div class="search-inner">
  1144.                 <i class="fas fa-search"></i>
  1145.                 <form method="get" action="{{ path('point_vente_index') }}" id="searchForm">
  1146.                     <input type="text" name="search" class="search-input"
  1147.                            placeholder="Rechercher un produit ou scanner un code-barres..."
  1148.                            value="{{ searchTerm }}" id="searchInput" autocomplete="off">
  1149.                 </form>
  1150.             </div>
  1151.         </div>
  1152.         {# Formulaire caché : ajout direct au panier via scan de code-barres #}
  1153.         <form id="scanBarcodeForm" action="{{ path('point_vente_scan_barcode') }}" method="POST" style="display:none;">
  1154.             <input type="hidden" name="code_barre" id="scanBarcodeInput">
  1155.             <input type="hidden" name="_token" value="{{ csrf_token('scan_barcode') }}">
  1156.         </form>
  1157.         {# Products grid #}
  1158.         <div class="products-scroll">
  1159.             <div class="product-grid">
  1160.                 {% for produit in produits %}
  1161.                     {% set prixAffichage = produit.prixVente ?: produit.puht %}
  1162.                     <div class="product-card" onclick="addToCartWithBeep('add-to-cart-{{ produit.id }}')">
  1163.                         <div class="product-img"
  1164.                              style="background-image:url('{% if produit.imageUrl %}{{ asset('uploads/' ~ produit.imageUrl) }}{% else %}{{ asset('uploads/images/default-product.png') }}{% endif %}')">
  1165.                             {% if produit.discountRate and produit.discountRate > 0 %}
  1166.                                 <span class="discount-badge">-{{ produit.discountRate }}%</span>
  1167.                             {% endif %}
  1168.                         </div>
  1169.                         <div class="product-body">
  1170.                             <div class="product-name">{{ produit.description }}</div>
  1171.                             <div class="product-price">{{ prixAffichage|number_format(3, '.', '') }} TND</div>
  1172.                             {% if produit.npuht %}
  1173.                                 <div class="product-price-buy">Achat : {{ produit.npuht|number_format(3, '.', '') }}</div>
  1174.                             {% endif %}
  1175.                         </div>
  1176.                         <form id="add-to-cart-{{ produit.id }}"
  1177.                               action="{{ path('point_vente_add_to_cart', {'id': produit.id}) }}"
  1178.                               method="POST" style="display:none;">
  1179.                             <input type="hidden" name="categorie" value="{{ selectedCategory }}">
  1180.                             <input type="hidden" name="search" value="{{ searchTerm }}">
  1181.                         </form>
  1182.                     </div>
  1183.                 {% else %}
  1184.                     <div class="no-products">
  1185.                         <i class="fas fa-box-open" style="font-size:32px;margin-bottom:8px;"></i>
  1186.                         <div>Aucun produit trouvé</div>
  1187.                     </div>
  1188.                 {% endfor %}
  1189.             </div>
  1190.         </div>
  1191.     </div>
  1192.     {# ========== CART SECTION ========== #}
  1193.     {% set modeResto = entreprise and entreprise.caisseModeRestaurant %}
  1194.     {% set enAttenteActif = entreprise is null or entreprise.caisseCommandesEnAttente %}
  1195.     <div class="pos-cart" id="posCart">
  1196.         <div class="cart-header">
  1197.             <i class="fas fa-shopping-cart"></i>
  1198.             {% if commande and commande.table %}
  1199.                 {{ commande.table.libelle }}
  1200.             {% elseif commande and commande.libelle %}
  1201.                 {{ commande.libelle }}
  1202.             {% else %}
  1203.                 Panier
  1204.             {% endif %}
  1205.             {% if panier|length > 0 %}
  1206.                 <span style="background:rgba(255,255,255,.25);border-radius:10px;padding:1px 8px;font-size:11px;margin-left:4px;">
  1207.                     {{ panier|length }} article{{ panier|length > 1 ? 's' : '' }}
  1208.                 </span>
  1209.             {% endif %}
  1210.             <button type="button" class="cart-close-btn" id="cartCloseBtn" aria-label="Fermer le panier">
  1211.                 <i class="fas fa-times"></i>
  1212.             </button>
  1213.         </div>
  1214.         {# Barre de contexte : tables, commandes en attente, type de commande #}
  1215.         {% if enAttenteActif or modeResto %}
  1216.             <div class="order-context">
  1217.                 <div class="order-context-links">
  1218.                     {% if entreprise and entreprise.caisseGestionTables %}
  1219.                         <a href="{{ path('point_vente_tables') }}" class="ctx-btn ctx-tables"><i class="fas fa-chair"></i> Tables</a>
  1220.                     {% endif %}
  1221.                     {% if enAttenteActif %}
  1222.                         <a href="{{ path('point_vente_en_attente') }}" class="ctx-btn">
  1223.                             <i class="fas fa-clock"></i> En attente
  1224.                             {% if nbEnAttente > 0 %}<span class="ctx-count">{{ nbEnAttente }}</span>{% endif %}
  1225.                         </a>
  1226.                     {% endif %}
  1227.                 </div>
  1228.                 {% if modeResto %}
  1229.                     <form method="POST" action="{{ path('point_vente_commande_infos') }}" class="order-context-form">
  1230.                         {% if commande and commande.table %}
  1231.                             <span class="ctx-info"><i class="fas fa-utensils"></i> Sur place</span>
  1232.                             {% if tablesTransfert is not empty %}
  1233.                                 <select class="ctx-select" aria-label="Transférer vers" form="transfertForm" name="table_id"
  1234.                                         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='';">
  1235.                                     <option value="">Transférer vers…</option>
  1236.                                     {% for t in tablesTransfert %}
  1237.                                         {% if t.id != commande.table.id %}<option value="{{ t.id }}">{{ t.salle.nom }} · {{ t.libelle }}</option>{% endif %}
  1238.                                     {% endfor %}
  1239.                                 </select>
  1240.                             {% endif %}
  1241.                         {% else %}
  1242.                             <select name="type_commande" class="ctx-select" onchange="this.form.submit()" aria-label="Type de commande">
  1243.                                 {% for code, libelle in typesCommande %}
  1244.                                     <option value="{{ code }}" {{ (commande ? commande.typeCommande : 'sur_place') == code ? 'selected' }}>{{ libelle }}</option>
  1245.                                 {% endfor %}
  1246.                             </select>
  1247.                         {% endif %}
  1248.                         <label class="ctx-couverts" title="Nombre de couverts">
  1249.                             <i class="fas fa-user-group"></i>
  1250.                             <input type="number" name="nb_couverts" min="0" max="99" value="{{ commande ? commande.nbCouverts : '' }}"
  1251.                                    placeholder="–" onchange="this.form.submit()">
  1252.                         </label>
  1253.                     </form>
  1254.                 {% endif %}
  1255.             </div>
  1256.         {% endif %}
  1257.         <form id="transfertForm" method="POST" action="{{ path('point_vente_transferer') }}" style="display:none;"></form>
  1258.         {# Cart items #}
  1259.         <div class="cart-body">
  1260.             {% if panier is empty %}
  1261.                 <div class="cart-empty">
  1262.                     <i class="fas fa-shopping-cart" style="font-size:36px;opacity:.25;"></i>
  1263.                     <span>Le panier est vide</span>
  1264.                 </div>
  1265.             {% else %}
  1266.                 {% for item in panier %}
  1267.                     {% set ligne = item.ligne %}
  1268.                     <div class="cart-item {{ item.tauxRemise > 0 ? 'has-discount' : '' }}">
  1269.                         <div class="cart-item-top">
  1270.                             <div class="cart-item-name">
  1271.                                 {{ ligne.libelle }}
  1272.                                 {% if ligne.envoyeeCuisine %}
  1273.                                     <span class="kitchen-badge" title="Envoyé en cuisine : {{ ligne.quantiteEnvoyeeCuisine }}">
  1274.                                         <i class="fas fa-fire-burner"></i>{% if ligne.quantiteAEnvoyer > 0 %} +{{ ligne.quantiteAEnvoyer }}{% endif %}
  1275.                                     </span>
  1276.                                 {% endif %}
  1277.                             </div>
  1278.                             <div class="cart-item-total">{{ item.totalHT|number_format(3, '.', '') }}</div>
  1279.                         </div>
  1280.                         <div class="cart-item-meta">
  1281.                             {{ item.prixApresRemise|number_format(3, '.', '') }} × {{ ligne.quantite }} TND
  1282.                             {% if item.tauxRemise > 0 %}
  1283.                                 &nbsp;·&nbsp;<span style="color:var(--pos-warning);font-weight:600;">-{{ item.tauxRemise }}%</span>
  1284.                             {% endif %}
  1285.                         </div>
  1286.                         {% if ligne.note %}
  1287.                             <div class="cart-item-note"><i class="fas fa-comment-dots"></i> {{ ligne.note }}</div>
  1288.                         {% endif %}
  1289.                         {# Quantity controls + remove #}
  1290.                         <div class="cart-item-controls">
  1291.                             <form action="{{ path('point_vente_update_cart', {'id': ligne.id}) }}" method="POST">
  1292.                                 <input type="hidden" name="quantite" value="{{ ligne.quantite - 1 }}">
  1293.                                 <button type="submit" class="qty-btn">−</button>
  1294.                             </form>
  1295.                             <span class="qty-display">{{ ligne.quantite }}</span>
  1296.                             <form action="{{ path('point_vente_update_cart', {'id': ligne.id}) }}" method="POST">
  1297.                                 <input type="hidden" name="quantite" value="{{ ligne.quantite + 1 }}">
  1298.                                 <button type="submit" class="qty-btn">+</button>
  1299.                             </form>
  1300.                             <form action="{{ path('point_vente_remove_from_cart', {'id': ligne.id}) }}" method="POST" style="margin-left:auto;">
  1301.                                 <button type="submit" class="btn-remove"><i class="fas fa-trash-alt" style="font-size:11px;"></i></button>
  1302.                             </form>
  1303.                         </div>
  1304.                         {# Remise et note : repliées pour garder le panier compact #}
  1305.                         <details class="line-options" {{ item.tauxRemise > 0 and ligne.remiseManuelle is not null ? 'open' }}>
  1306.                             <summary>{{ modeResto ? 'Remise / note cuisine' : 'Remise' }}</summary>
  1307.                             <form action="{{ path('point_vente_apply_discount', {'id': ligne.id}) }}"
  1308.                                   method="POST" class="discount-form">
  1309.                                 <div class="discount-row">
  1310.                                     <i class="fas fa-tag" style="font-size:10px;color:var(--pos-gray-400);"></i>
  1311.                                     <input type="number" name="remise" class="discount-input-small"
  1312.                                            value="{{ item.tauxRemise }}" min="0" max="100" step="0.1"
  1313.                                            placeholder="Remise %">
  1314.                                     <button type="submit" class="discount-submit-btn">Appliquer</button>
  1315.                                 </div>
  1316.                             </form>
  1317.                             {% if modeResto %}
  1318.                                 <form action="{{ path('point_vente_ligne_note', {'id': ligne.id}) }}" method="POST" class="note-form">
  1319.                                     <div class="discount-row">
  1320.                                         <i class="fas fa-comment-dots" style="font-size:10px;color:var(--pos-gray-400);"></i>
  1321.                                         <input type="text" name="note" class="discount-input-small note-input" maxlength="255"
  1322.                                                value="{{ ligne.note }}" placeholder="Note cuisine (sans oignon...)">
  1323.                                         <button type="submit" class="discount-submit-btn">OK</button>
  1324.                                     </div>
  1325.                                 </form>
  1326.                             {% endif %}
  1327.                         </details>
  1328.                     </div>
  1329.                 {% endfor %}
  1330.             {% endif %}
  1331.         </div>
  1332.         {# ========== FOOTER : SUMMARY + ACTIONS ========== #}
  1333.         <div class="cart-footer">
  1334.             <div class="cart-summary">
  1335.                 {# HT #}
  1336.                 <div class="summary-row">
  1337.                     <span>Sous-total HT</span>
  1338.                     <span>{{ totalPanier|number_format(3, '.', '') }} TND</span>
  1339.                 </div>
  1340.                 {# TVA par taux #}
  1341.                 {% for detail in detailsTva %}
  1342.                     <div class="summary-row">
  1343.                         <span>TVA {{ detail.taux }}%</span>
  1344.                         <span>{{ detail.montant|number_format(3, '.', '') }} TND</span>
  1345.                     </div>
  1346.                 {% else %}
  1347.                     <div class="summary-row">
  1348.                         <span>Total TVA</span>
  1349.                         <span>{{ totalTva|number_format(3, '.', '') }} TND</span>
  1350.                     </div>
  1351.                 {% endfor %}
  1352.                 {% if timbre > 0 %}
  1353.                     <div class="summary-row">
  1354.                         <span>Droit de timbre</span>
  1355.                         <span>{{ timbre|number_format(3, '.', '') }} TND</span>
  1356.                     </div>
  1357.                 {% endif %}
  1358.                 {# TTC #}
  1359.                 <div class="summary-row ttc-row">
  1360.                     <span>Total TTC</span>
  1361.                     <span id="total">{{ (totalTTC + timbre)|number_format(3, '.', '') }} TND</span>
  1362.                 </div>
  1363.                 {# Remise globale #}
  1364.                 <div class="global-discount-block">
  1365.                     <div class="global-discount-label">
  1366.                         <i class="fas fa-scissors"></i>
  1367.                         Remise globale (DT)
  1368.                     </div>
  1369.                     <div class="global-discount-inputs">
  1370.                         <input type="number"
  1371.                                id="global_discount"
  1372.                                name="global_discount"
  1373.                                min="0"
  1374.                                max="{{ totalTTC }}"
  1375.                                step="0.001"
  1376.                                placeholder="0.000"
  1377.                                class="global-discount-input"
  1378.                                autocomplete="off"
  1379.                                oninput="updateTotalNet(this.value)">
  1380.                         <span class="global-discount-unit">DT</span>
  1381.                         <span id="remise-display" class="global-discount-amount" style="display:none;"></span>
  1382.                     </div>
  1383.                 </div>
  1384.                 {# Total net (affiché si remise > 0) #}
  1385.                 <div class="total-net-row" id="total-net-block" style="display:none;">
  1386.                     <span class="total-net-label">TOTAL NET À PAYER</span>
  1387.                     <span class="total-net-value" id="total-net-value">0.000 TND</span>
  1388.                 </div>
  1389.             </div>
  1390.             {# ========== ACTION BUTTONS ========== #}
  1391.             <div class="cart-actions">
  1392.                 {% if canEncaisser %}
  1393.                     {# Bouton principal #}
  1394.                     <button class="btn-primary-action" id="processPaymentBtn">
  1395.                         <i class="fas fa-receipt"></i>
  1396.                         Valider &amp; imprimer ticket
  1397.                     </button>
  1398.                 {% endif %}
  1399.                 {# Formulaire ticket (caché) #}
  1400.                 <form method="POST" action="{{ path('point_vente_valider_commande_ticket') }}"
  1401.                       id="ticketForm" style="display:none;">
  1402.                     <input type="hidden" name="selectedClientId" id="selectedClientIdTicket" value="">
  1403.                     <input type="hidden" name="payment_method" value="espece">
  1404.                     <input type="hidden" name="montant_recu" value="">
  1405.                     <input type="hidden" name="monnaie_rendre" value="">
  1406.                     <input type="hidden" name="global_discount" id="ticketFormDiscount" value="0">
  1407.                     <input type="hidden" name="_token" value="{{ csrf_token('validate_order') }}">
  1408.                 </form>
  1409.                 {# Formulaire paiement (caché) #}
  1410.                 <form method="POST" action="{{ path('point_vente_valider_commande') }}"
  1411.                       id="paymentForm" style="display:none;">
  1412.                     <input type="hidden" name="selectedespeceClientId" id="selectedespeceClientId" value="">
  1413.                     <input type="hidden" name="payment_method" value="">
  1414.                     <input type="hidden" name="montant_recu" value="">
  1415.                     <input type="hidden" name="monnaie_rendre" value="">
  1416.                     <input type="hidden" name="global_discount" id="paymentFormDiscount" value="0">
  1417.                     <input type="hidden" name="_token" value="{{ csrf_token('validate_order') }}">
  1418.                 </form>
  1419.                 {% if canEncaisser %}
  1420.                     {# Row 1 : moyens de paiement #}
  1421.                     <div class="action-row" style="grid-template-columns:1fr 1fr 1fr;">
  1422.                         <button class="btn-action cash" onclick="openCashModal('normal')">
  1423.                             <i class="fas fa-money-bill-wave" style="font-size:15px;"></i>
  1424.                             Espèces
  1425.                         </button>
  1426.                         <button class="btn-action card" data-payment="carte">
  1427.                             <i class="fas fa-credit-card" style="font-size:15px;"></i>
  1428.                             Carte
  1429.                         </button>
  1430.                         <button class="btn-action unpaid" data-payment="nonPaye">
  1431.                             <i class="fas fa-times" style="font-size:15px;"></i>
  1432.                             Non payé
  1433.                         </button>
  1434.                     </div>
  1435.                 {% else %}
  1436.                     <div class="no-cash-right">
  1437.                         <i class="fas fa-circle-info"></i>
  1438.                         L'encaissement est fait par un responsable. Envoyez la commande en cuisine
  1439.                         ou mettez-la en attente / sur une table.
  1440.                     </div>
  1441.                 {% endif %}
  1442.                 {# Restaurant : cuisine, pré-addition, addition partagée #}
  1443.                 {% if modeResto and commande and not commande.vide %}
  1444.                     <div class="action-row" style="grid-template-columns:repeat({{ entreprise.caisseTicketCuisine ? 3 : 2 }}, 1fr);">
  1445.                         {% if entreprise.caisseTicketCuisine %}
  1446.                             <form method="POST" action="{{ path('point_vente_bon_cuisine') }}" target="_blank" style="display:contents;">
  1447.                                 <button type="submit" class="btn-action kitchen {{ commande.articlesNonEnvoyesCuisine ? 'pending' : '' }}"
  1448.                                         onclick="setTimeout(function(){ window.location.reload(); }, 800);">
  1449.                                     <i class="fas fa-fire-burner" style="font-size:14px;"></i>
  1450.                                     Cuisine
  1451.                                 </button>
  1452.                             </form>
  1453.                         {% endif %}
  1454.                         <a href="{{ path('point_vente_pre_addition') }}" target="_blank" class="btn-action client" style="text-decoration:none;">
  1455.                             <i class="fas fa-file-invoice" style="font-size:14px;"></i>
  1456.                             Pré-addition
  1457.                         </a>
  1458.                         <a href="{{ path('point_vente_separer') }}" class="btn-action add-prod" style="text-decoration:none;">
  1459.                             <i class="fas fa-divide" style="font-size:14px;"></i>
  1460.                             Partager
  1461.                         </a>
  1462.                     </div>
  1463.                 {% endif %}
  1464.                 {# Mise en attente / table #}
  1465.                 {% if commande and not commande.vide and (enAttenteActif or (commande.table)) %}
  1466.                     <form method="POST" action="{{ path('point_vente_mettre_en_attente') }}" class="hold-form">
  1467.                         {% if commande.table %}
  1468.                             <button type="submit" class="btn-action hold" style="width:100%;">
  1469.                                 <i class="fas fa-chair" style="font-size:14px;"></i>
  1470.                                 Garder sur la {{ commande.table.libelle }} et revenir au plan
  1471.                             </button>
  1472.                         {% else %}
  1473.                             <input type="text" name="libelle" class="hold-input" maxlength="120"
  1474.                                    value="{{ commande.libelle }}" placeholder="Nom (optionnel)">
  1475.                             <button type="submit" class="btn-action hold">
  1476.                                 <i class="fas fa-pause" style="font-size:14px;"></i>
  1477.                                 Mettre en attente
  1478.                             </button>
  1479.                         {% endif %}
  1480.                     </form>
  1481.                 {% endif %}
  1482.                 {# Row 2 : client + produit + catégorie #}
  1483.                 <div class="action-row" style="grid-template-columns:1fr 1fr 1fr;">
  1484.                     <button class="btn-action client" id="addClientBtn">
  1485.                         <i class="fas fa-user-plus" style="font-size:14px;"></i>
  1486.                         Client
  1487.                     </button>
  1488.                     <button class="btn-action add-prod"
  1489.                             data-bs-toggle="modal" data-bs-target="#addProductFromCaisseModal">
  1490.                         <i class="fas fa-plus-circle" style="font-size:14px;"></i>
  1491.                         Produit
  1492.                     </button>
  1493.                     <button class="btn-action add-cat"
  1494.                             data-bs-toggle="modal" data-bs-target="#addCategoryFromCaisseModal">
  1495.                         <i class="fas fa-tags" style="font-size:14px;"></i>
  1496.                         Catégorie
  1497.                     </button>
  1498.                 </div>
  1499.                 {# Vider le panier — séparé et discret #}
  1500.                 <button class="btn-action danger" style="width:100%;margin-top:4px;" onclick="confirmClearCart()">
  1501.                     <i class="fas fa-trash-alt" style="font-size:14px;"></i>
  1502.                     {{ commande and commande.table ? 'Libérer la table' : 'Vider le panier' }}
  1503.                 </button>
  1504.                 <form id="clearCartForm" method="POST"
  1505.                       action="{{ path('point_vente_vider_panier') }}" style="display:none;">
  1506.                 </form>
  1507.             </div>
  1508.         </div>
  1509.     </div>
  1510. </div>
  1511. {# ========== PANIER MOBILE : BACKDROP + BOUTON FLOTTANT ========== #}
  1512. <div id="cartBackdrop" class="cart-backdrop"></div>
  1513. <button type="button" class="cart-fab" id="cartFab">
  1514.     <i class="fas fa-shopping-cart"></i>
  1515.     <span id="fabTotal">{{ totalTTC|number_format(3, '.', '') }} TND</span>
  1516.     {% if panier|length > 0 %}
  1517.         <span class="fab-count" id="fabCount">{{ panier|length }}</span>
  1518.     {% endif %}
  1519. </button>
  1520. {# ========== MODAL PAIEMENT ESPÈCES ========== #}
  1521. <div id="cashPaymentModal" class="pos-modal-overlay">
  1522.     <div class="pos-modal">
  1523.         <div class="pos-modal-header">
  1524.             <span><i class="fas fa-money-bill-wave me-2"></i>Paiement en espèces</span>
  1525.             <button class="pos-modal-close" onclick="closeCashModal()">×</button>
  1526.         </div>
  1527.         <div class="pos-modal-body">
  1528.             <div class="cash-amount-box">
  1529.                 <span class="cash-label">Total à payer</span>
  1530.                 <span class="cash-value" id="cash-total">0.000</span>
  1531.                 <span style="font-size:13px;color:var(--pos-gray-400);"> TND</span>
  1532.             </div>
  1533.             <div class="cash-amount-box" style="padding:10px 16px;">
  1534.                 <span class="cash-label">Montant reçu</span>
  1535.                 <input type="number" id="cash-received" class="cash-input"
  1536.                        placeholder="0.000" step="0.001" min="0"
  1537.                        oninput="calculateChange()">
  1538.             </div>
  1539.             <button type="button" class="exact-amount-btn" onclick="setExactAmount()">
  1540.                 <i class="fas fa-check-circle"></i>
  1541.                 Montant exact
  1542.             </button>
  1543.             <div class="quick-btns">
  1544.                 <button class="quick-btn" onclick="addAmount(100)">100</button>
  1545.                 <button class="quick-btn" onclick="addAmount(50)">50</button>
  1546.                 <button class="quick-btn" onclick="addAmount(20)">20</button>
  1547.                 <button class="quick-btn" onclick="addAmount(10)">10</button>
  1548.                 <button class="quick-btn" onclick="addAmount(5)">5</button>
  1549.                 <button class="quick-btn" onclick="addAmount(1)">1</button>
  1550.             </div>
  1551.             <div class="cash-amount-box highlight">
  1552.                 <span class="cash-label">Monnaie à rendre</span>
  1553.                 <span class="cash-value" id="cash-change" style="color:var(--pos-warning);">0.000</span>
  1554.                 <span style="font-size:13px;color:var(--pos-gray-400);"> TND</span>
  1555.             </div>
  1556.             <div class="modal-action-row">
  1557.                 <button class="btn-modal-cancel" onclick="closeCashModal()">Annuler</button>
  1558.                 <button class="btn-modal-confirm" onclick="confirmCashPayment()">
  1559.                     <i class="fas fa-check me-1"></i>Valider
  1560.                 </button>
  1561.             </div>
  1562.         </div>
  1563.     </div>
  1564. </div>
  1565. {# ========== MODAL CLIENT ========== #}
  1566. <div id="customerModal" class="pos-modal-overlay">
  1567.     <div class="pos-modal">
  1568.         <div class="pos-modal-header">
  1569.             <span><i class="fas fa-users me-2"></i>Sélectionner un client</span>
  1570.             <button class="pos-modal-close" onclick="closeModal('customerModal')">×</button>
  1571.         </div>
  1572.         <div class="pos-modal-body" style="padding:0;">
  1573.             <ul class="client-list">
  1574.                 {% for client in clients %}
  1575.                     <li class="client-item" data-client-id="{{ client.id }}"
  1576.                         data-client-name="{{ client.nom }} {{ client.prenom }}">
  1577.                         <span>{{ client.nom }} {{ client.prenom }}</span>
  1578.                         <span style="font-size:11px;color:var(--pos-gray-400);">{{ client.telephone }}</span>
  1579.                     </li>
  1580.                 {% else %}
  1581.                     <li style="padding:20px;text-align:center;font-size:13px;color:var(--pos-gray-400);">
  1582.                         Aucun client trouvé
  1583.                     </li>
  1584.                 {% endfor %}
  1585.             </ul>
  1586.         </div>
  1587.     </div>
  1588. </div>
  1589. {# ========== MODAL AJOUT CLIENT ========== #}
  1590. <div id="addClientModal" class="pos-modal-overlay">
  1591.     <div class="pos-modal" style="max-width:500px;">
  1592.         <div class="pos-modal-header">
  1593.             <span><i class="fas fa-user-plus me-2"></i>Nouveau client</span>
  1594.             <button class="pos-modal-close" onclick="closeModal('addClientModal')">×</button>
  1595.         </div>
  1596.         <div class="pos-modal-body">
  1597.             {{ form_start(form, {'attr': {'action': path('client_new_modal')}}) }}
  1598.                 {{ form_widget(form) }}
  1599.                 <div class="modal-action-row">
  1600.                     <button type="button" class="btn-modal-cancel" onclick="closeModal('addClientModal')">Annuler</button>
  1601.                     <button type="submit" class="btn-modal-confirm">Enregistrer</button>
  1602.                 </div>
  1603.             {{ form_end(form) }}
  1604.         </div>
  1605.     </div>
  1606. </div>
  1607. {# ========== MODAL CAISSE FERMÉE ========== #}
  1608. <div id="noCaisseSessionModal" class="pos-modal-overlay">
  1609.     <div class="pos-modal" style="max-width:420px;">
  1610.         <div class="pos-modal-header" style="background:var(--pos-warning);">
  1611.             <span><i class="fas fa-triangle-exclamation me-2"></i>Caisse fermée</span>
  1612.             <button class="pos-modal-close" onclick="closeModal('noCaisseSessionModal')">×</button>
  1613.         </div>
  1614.         <div class="pos-modal-body" style="text-align:center;">
  1615.             <i class="fas fa-cash-register" style="font-size:38px;color:var(--pos-warning);margin-bottom:12px;"></i>
  1616.             <p style="font-size:14.5px;color:var(--pos-gray-800);margin-bottom:0;">
  1617.                 Tu dois d'abord <strong>ouvrir la caisse</strong> avant de pouvoir encaisser des ventes.
  1618.             </p>
  1619.             <div class="modal-action-row">
  1620.                 <button type="button" class="btn-modal-cancel" onclick="closeModal('noCaisseSessionModal')">Annuler</button>
  1621.                 <a href="{{ path('caisse_cloture_ouvrir') }}" class="btn-modal-confirm" style="text-decoration:none;text-align:center;">
  1622.                     Ouvrir la caisse
  1623.                 </a>
  1624.             </div>
  1625.         </div>
  1626.     </div>
  1627. </div>
  1628. {# ========== MODAL NOUVEAU PRODUIT ========== #}
  1629. <div class="modal fade" id="addProductFromCaisseModal" tabindex="-1" aria-hidden="true">
  1630.     <div class="modal-dialog modal-dialog-centered mw-650px">
  1631.         <div class="modal-content">
  1632.             <div class="modal-header" style="background:var(--pos-primary);color:#fff;">
  1633.                 <h5 class="modal-title">Nouveau produit</h5>
  1634.                 <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
  1635.             </div>
  1636.             <form method="post" action="{{ path('app_produit_new_from_caisse') }}" enctype="multipart/form-data">
  1637.                 <div class="modal-body">
  1638.                     {{ render(path('app_produit_new_from_caisse')) }}
  1639.                 </div>
  1640.                 <div class="modal-footer">
  1641.                     <button type="button" class="btn btn-light" data-bs-dismiss="modal">Annuler</button>
  1642.                 </div>
  1643.             </form>
  1644.         </div>
  1645.     </div>
  1646. </div>
  1647. {# ========== MODAL NOUVELLE CATÉGORIE ========== #}
  1648. <div class="modal fade" id="addCategoryFromCaisseModal" tabindex="-1" aria-hidden="true">
  1649.     <div class="modal-dialog modal-dialog-centered">
  1650.         <div class="modal-content">
  1651.             <div class="modal-header" style="background:var(--pos-primary);color:#fff;">
  1652.                 <h5 class="modal-title">Nouvelle catégorie</h5>
  1653.                 <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
  1654.             </div>
  1655.             <form method="post" action="{{ path('app_categorie_new_from_caisse') }}">
  1656.                 <div class="modal-body">
  1657.                     {{ render(path('app_categorie_new_from_caisse')) }}
  1658.                 </div>
  1659.             </form>
  1660.         </div>
  1661.     </div>
  1662. </div>
  1663. {# ========== JAVASCRIPT ========== #}
  1664. <script>
  1665. /* ---- CONSTANTES ---- */
  1666. const TTC_BRUT = {{ totalTTC + timbre }};
  1667. const CAISSE_SESSION_OUVERTE = {{ sessionCaisse ? 'true' : 'false' }};
  1668. /* ============================================================
  1669.    SIGNAL SONORE (bip caisse à l'ajout d'un produit au panier)
  1670.    ============================================================ */
  1671. var __caissePosAudioCtx = null;
  1672. function playCaisseBeep() {
  1673.     try {
  1674.         var AudioCtx = window.AudioContext || window.webkitAudioContext;
  1675.         if (!AudioCtx) return;
  1676.         if (!__caissePosAudioCtx) __caissePosAudioCtx = new AudioCtx();
  1677.         if (__caissePosAudioCtx.state === 'suspended') __caissePosAudioCtx.resume();
  1678.         var ctx = __caissePosAudioCtx;
  1679.         var osc = ctx.createOscillator();
  1680.         var gain = ctx.createGain();
  1681.         osc.connect(gain);
  1682.         gain.connect(ctx.destination);
  1683.         osc.type = 'square';
  1684.         osc.frequency.setValueAtTime(1568, ctx.currentTime);
  1685.         gain.gain.setValueAtTime(0.0001, ctx.currentTime);
  1686.         gain.gain.exponentialRampToValueAtTime(0.2, ctx.currentTime + 0.01);
  1687.         gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.11);
  1688.         osc.start(ctx.currentTime);
  1689.         osc.stop(ctx.currentTime + 0.12);
  1690.     } catch (e) {
  1691.         /* Audio non disponible (navigateur, permissions...) : on ignore silencieusement */
  1692.     }
  1693. }
  1694. /**
  1695.  * Joue le bip caisse puis soumet le formulaire d'ajout au panier,
  1696.  * avec un très court délai pour laisser le son démarrer avant le
  1697.  * rechargement de page.
  1698.  */
  1699. function addToCartWithBeep(formId) {
  1700.     if (!CAISSE_SESSION_OUVERTE) {
  1701.         openModal('noCaisseSessionModal');
  1702.         return;
  1703.     }
  1704.     playCaisseBeep();
  1705.     var form = document.getElementById(formId);
  1706.     if (form) {
  1707.         window.setTimeout(function () { form.submit(); }, 90);
  1708.     }
  1709. }
  1710. /* ============================================================
  1711.    REMISE GLOBALE
  1712.    ============================================================ */
  1713. function getEffectiveTotal() {
  1714.     const netBlock = document.getElementById('total-net-block');
  1715.     if (netBlock && netBlock.style.display !== 'none') {
  1716.         const raw = document.getElementById('total-net-value').textContent.replace(' TND','').trim();
  1717.         return parseFloat(raw) || 0;
  1718.     }
  1719.     return TTC_BRUT;
  1720. }
  1721. function syncDiscount() {
  1722.     const val = parseFloat(document.getElementById('global_discount').value) || 0;
  1723.     document.getElementById('paymentFormDiscount').value = val.toFixed(3);
  1724.     document.getElementById('ticketFormDiscount').value  = val.toFixed(3);
  1725. }
  1726. function updateTotalNet(remise) {
  1727.     const r   = parseFloat(remise) || 0;
  1728.     const net = Math.max(0, TTC_BRUT - r);
  1729.     const netBlock      = document.getElementById('total-net-block');
  1730.     const netValue      = document.getElementById('total-net-value');
  1731.     const remiseDisplay = document.getElementById('remise-display');
  1732.     const cashTotal     = document.getElementById('cash-total');
  1733.     if (r > 0) {
  1734.         netBlock.style.display = 'flex';
  1735.         netValue.textContent   = net.toFixed(3) + ' TND';
  1736.         if (remiseDisplay) {
  1737.             remiseDisplay.textContent = '– ' + r.toFixed(3) + ' DT';
  1738.             remiseDisplay.style.display = 'inline';
  1739.         }
  1740.         if (cashTotal) cashTotal.textContent = net.toFixed(3);
  1741.     } else {
  1742.         netBlock.style.display = 'none';
  1743.         if (remiseDisplay) remiseDisplay.style.display = 'none';
  1744.         if (cashTotal) cashTotal.textContent = TTC_BRUT.toFixed(3);
  1745.     }
  1746.     syncDiscount();
  1747. }
  1748. /* ============================================================
  1749.    MODALE ESPÈCES
  1750.    ============================================================ */
  1751. function openCashModal(mode) {
  1752.     const modal    = document.getElementById('cashPaymentModal');
  1753.     const effective = getEffectiveTotal();
  1754.     modal.dataset.mode = mode || 'normal';
  1755.     document.getElementById('cash-total').textContent    = effective.toFixed(3);
  1756.     document.getElementById('cash-received').value       = '';
  1757.     document.getElementById('cash-change').textContent   = '0.000';
  1758.     modal.classList.add('open');
  1759.     setTimeout(() => document.getElementById('cash-received').focus(), 120);
  1760. }
  1761. function closeCashModal() {
  1762.     document.getElementById('cashPaymentModal').classList.remove('open');
  1763. }
  1764. function calculateChange() {
  1765.     const total    = parseFloat(document.getElementById('cash-total').textContent) || 0;
  1766.     const received = parseFloat(document.getElementById('cash-received').value)    || 0;
  1767.     const change   = Math.max(0, received - total);
  1768.     document.getElementById('cash-change').textContent = change.toFixed(3);
  1769. }
  1770. function addAmount(amount) {
  1771.     const input   = document.getElementById('cash-received');
  1772.     const current = parseFloat(input.value) || 0;
  1773.     input.value   = (current + amount).toFixed(3);
  1774.     calculateChange();
  1775. }
  1776. function setExactAmount() {
  1777.     const total = parseFloat(document.getElementById('cash-total').textContent) || 0;
  1778.     document.getElementById('cash-received').value = total.toFixed(3);
  1779.     calculateChange();
  1780. }
  1781. function confirmCashPayment() {
  1782.     const modal    = document.getElementById('cashPaymentModal');
  1783.     const mode     = modal.dataset.mode || 'normal';
  1784.     const total    = parseFloat(document.getElementById('cash-total').textContent)  || 0;
  1785.     const received = parseFloat(document.getElementById('cash-received').value)     || 0;
  1786.     if (!received || isNaN(received)) {
  1787.         alert('Veuillez entrer un montant reçu.');
  1788.         return;
  1789.     }
  1790.     if (received < total) {
  1791.         if (!confirm('Le montant reçu est inférieur au total. Continuer ?')) return;
  1792.     }
  1793.     const change = Math.max(0, received - total);
  1794.     syncDiscount();
  1795.     updatePaymentForms(received, change);
  1796.     closeCashModal();
  1797.     if (mode === 'ticket') {
  1798.         processTicketPayment();
  1799.     } else {
  1800.         document.getElementById('paymentForm').submit();
  1801.     }
  1802. }
  1803. function updatePaymentForms(received, change) {
  1804.     const montantRecu   = received.toFixed(3);
  1805.     const monnaieRendre = change.toFixed(3);
  1806.     const pf = document.getElementById('paymentForm');
  1807.     pf.querySelector('[name="payment_method"]').value  = 'espece';
  1808.     pf.querySelector('[name="montant_recu"]').value    = montantRecu;
  1809.     pf.querySelector('[name="monnaie_rendre"]').value  = monnaieRendre;
  1810.     const tf = document.getElementById('ticketForm');
  1811.     tf.querySelector('[name="payment_method"]').value  = 'espece';
  1812.     tf.querySelector('[name="montant_recu"]').value    = montantRecu;
  1813.     tf.querySelector('[name="monnaie_rendre"]').value  = monnaieRendre;
  1814. }
  1815. /* ============================================================
  1816.    BOUTON VALIDER + TICKET
  1817.    ============================================================ */
  1818. document.getElementById('processPaymentBtn')?.addEventListener('click', function () {
  1819.     openCashModal('ticket');
  1820. });
  1821. function processTicketPayment() {
  1822.     const form = document.getElementById('ticketForm');
  1823.     // Synchroniser le client sélectionné dans le formulaire ticket
  1824.     const clientId   = document.getElementById('selectedClientId')?.value || '';
  1825.     const clientName = document.getElementById('clientNameDisplay')?.textContent || '';
  1826.     const ticketClientField = document.getElementById('selectedClientIdTicket');
  1827.     if (ticketClientField && clientId) {
  1828.         ticketClientField.value = clientId;
  1829.     }
  1830.     const formData = new FormData(form);
  1831.     // Garantie supplémentaire : écraser dans le formData directement
  1832.     if (clientId) formData.set('selectedClientId', clientId);
  1833.     if (clientName && clientName !== 'Client Comptant') formData.set('nomClientChoisi', clientName);
  1834.     fetch(form.action, {
  1835.         method: 'POST',
  1836.         body: formData,
  1837.         headers: { 'X-Requested-With': 'XMLHttpRequest' }
  1838.     })
  1839.     .then(r => { if (!r.ok) return r.json().then(e => { throw e; }); return r.json(); })
  1840.     .then(data => {
  1841.         if (data.success) {
  1842.             printTicket(data.ticketData);
  1843.             setTimeout(() => { window.location.href = "{{ path('point_vente_index') }}"; }, 1200);
  1844.         } else {
  1845.             throw new Error(data.message || 'Erreur lors de la validation');
  1846.         }
  1847.     })
  1848.     .catch(err => alert('Erreur : ' + (err.message || 'Communication serveur échouée')));
  1849. }
  1850. /* ============================================================
  1851.    NON PAYÉ et autres méthodes
  1852.    ============================================================ */
  1853. document.querySelectorAll('[data-payment]').forEach(btn => {
  1854.     btn.addEventListener('click', function () {
  1855.         const method = this.dataset.payment;
  1856.         const form   = document.getElementById('paymentForm');
  1857.         form.querySelector('[name="payment_method"]').value = method;
  1858.         syncDiscount();
  1859.         if (confirm('Confirmer le paiement "' + this.textContent.trim() + '" ?')) {
  1860.             form.submit();
  1861.         }
  1862.     });
  1863. });
  1864. /* ============================================================
  1865.    IMPRESSION TICKET
  1866.    ============================================================ */
  1867. function printTicket(ticketData) {
  1868.     const win = window.open('', '_blank', 'width=420,height=650,menubar=no,toolbar=no,location=no');
  1869.     if (!win) { alert('Autorisez les pop-ups pour imprimer le ticket.'); return; }
  1870.     const td = {
  1871.         nomEntreprise: ticketData.nomEntreprise || 'VOTRE ENTREPRISE',
  1872.         date:          ticketData.date          || new Date().toLocaleDateString('fr-FR'),
  1873.         reference:     ticketData.reference     || 'N/A',
  1874.         nomClient:     ticketData.nomClient     || 'Client Comptant',
  1875.         items:         ticketData.items         || [],
  1876.         totalHT:       parseFloat(ticketData.totalHT)   || 0,
  1877.         tva:           parseFloat(ticketData.tva)        || 0,
  1878.         totalTTC:      parseFloat(ticketData.totalTTC)   || 0,
  1879.         montantRecu:   parseFloat(ticketData.montantRecu)|| 0,
  1880.         monnaieRendre: parseFloat(ticketData.monnaieRendre) || 0,
  1881.         detailsTva:    ticketData.detailsTva    || {},
  1882.         globalDiscount:parseFloat(ticketData.globalDiscount) || 0,
  1883.         totalRemise:   parseFloat(ticketData.totalRemise) || 0,
  1884.         timbre:        parseFloat(ticketData.timbre)      || 0
  1885.     };
  1886.     let tvaHtml = '';
  1887.     for (const [, detail] of Object.entries(td.detailsTva)) {
  1888.         tvaHtml += `
  1889.             <div class="row"><span>Base HT ${detail.taux}% :</span><span>${parseFloat(detail.base).toFixed(3)} TND</span></div>
  1890.             <div class="row"><span>TVA ${detail.taux}% :</span><span>${parseFloat(detail.montant).toFixed(3)} TND</span></div>`;
  1891.     }
  1892.     const discountSection = (td.globalDiscount > 0) ? `
  1893.         <div class="row" style="color:#c00;font-weight:600;">
  1894.             <span>Remise globale :</span><span>– ${td.globalDiscount.toFixed(3)} TND</span>
  1895.         </div>
  1896.         <div class="row final" style="color:#065f46;border-top:1px dashed #000;margin-top:4px;padding-top:4px;">
  1897.             <span>TOTAL NET TTC :</span><span>${td.totalTTC.toFixed(3)} TND</span>
  1898.         </div>` : `
  1899.         <div class="row final"><span>TOTAL TTC :</span><span>${td.totalTTC.toFixed(3)} TND</span></div>`;
  1900.     const timbreHtml = td.timbre > 0
  1901.         ? `<div class="row"><span>Timbre fiscal :</span><span>${td.timbre.toFixed(3)} TND</span></div>` : '';
  1902.     const remiseLignesHtml = td.totalRemise > 0
  1903.         ? `<div class="row" style="color:#c00;"><span>Remises lignes :</span><span>– ${td.totalRemise.toFixed(3)} TND</span></div>` : '';
  1904.     win.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
  1905.     <title>Ticket ${td.reference}</title>
  1906.     <style>
  1907.         @page { size:80mm auto; margin:0; }
  1908.         body { font-family:'Courier New',monospace; font-size:11px; margin:0; padding:0; width:80mm; }
  1909.         .ticket { width:74mm; margin:0 auto; padding:4mm 3mm; }
  1910.         .header { text-align:center; border-bottom:1px dashed #000; padding-bottom:6px; margin-bottom:6px; }
  1911.         .company { font-size:15px; font-weight:900; letter-spacing:1px; text-transform:uppercase; }
  1912.         .info { font-size:10px; margin:2px 0; }
  1913.         .items { margin:6px 0; }
  1914.         .item-head { display:flex; justify-content:space-between; font-weight:700; font-size:10px;
  1915.                      border-bottom:1px solid #000; padding-bottom:3px; margin-bottom:3px; }
  1916.         .item-row { display:flex; justify-content:space-between; font-size:10px; margin-bottom:2px; }
  1917.         .desc { flex:2; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; padding-right:4px; }
  1918.         .qty  { width:12%; text-align:center; }
  1919.         .pu   { width:20%; text-align:right; }
  1920.         .tot  { width:20%; text-align:right; }
  1921.         .totals { border-top:1px dashed #000; border-bottom:1px dashed #000; padding:5px 0; margin:5px 0; }
  1922.         .row { display:flex; justify-content:space-between; font-size:11px; margin-bottom:2px; }
  1923.         .row.final { font-weight:900; font-size:13px; }
  1924.         .payment { font-size:10px; margin:4px 0; }
  1925.         .footer { text-align:center; border-top:1px dashed #000; padding-top:6px; margin-top:8px; font-size:9px; }
  1926.         .merci { font-size:12px; font-weight:900; }
  1927.     </style></head><body><div class="ticket">
  1928.     <div class="header">
  1929.         <div class="company">${td.nomEntreprise}</div>
  1930.         <div class="info">${td.date}</div>
  1931.         <div class="info">N° ${td.reference}</div>
  1932.         <div class="info">Client : ${td.nomClient}</div>
  1933.     </div>
  1934.     <div class="items">
  1935.         <div class="item-head">
  1936.             <span class="desc">Article</span>
  1937.             <span class="qty">Qté</span>
  1938.             <span class="pu">P.U</span>
  1939.             <span class="tot">Total</span>
  1940.         </div>
  1941.         ${td.items.map(i => `
  1942.         <div class="item-row">
  1943.             <span class="desc" title="${i.description}">${i.description}</span>
  1944.             <span class="qty">${i.quantite}</span>
  1945.             <span class="pu">${parseFloat(i.prixUnitaire).toFixed(3)}</span>
  1946.             <span class="tot">${parseFloat(i.total).toFixed(3)}</span>
  1947.         </div>
  1948.         ${parseFloat(i.tauxRemise) > 0 ? `<div class="item-row" style="color:#c00;font-size:9px;"><span class="desc">  └ remise ${parseFloat(i.tauxRemise)}%</span></div>` : ''}
  1949.         `).join('')}
  1950.     </div>
  1951.     <div class="totals">
  1952.         ${tvaHtml}
  1953.         <div class="row"><span>Total HT :</span><span>${td.totalHT.toFixed(3)} TND</span></div>
  1954.         <div class="row"><span>Total TVA :</span><span>${td.tva.toFixed(3)} TND</span></div>
  1955.         ${remiseLignesHtml}
  1956.         ${timbreHtml}
  1957.         ${discountSection}
  1958.     </div>
  1959.     ${td.montantRecu > 0 ? `
  1960.     <div class="payment">
  1961.         <div class="row"><span>Montant reçu :</span><span>${td.montantRecu.toFixed(3)} TND</span></div>
  1962.         <div class="row"><span>Monnaie rendue :</span><span>${td.monnaieRendre.toFixed(3)} TND</span></div>
  1963.     </div>` : ''}
  1964.     <div class="footer">
  1965.         <div class="merci">MERCI DE VOTRE VISITE</div>
  1966.         <div>www.mazego.fr</div>
  1967.         <div>Imprimé le ${new Date().toLocaleString('fr-FR')}</div>
  1968.     </div>
  1969.     </div>
  1970.     <script>window.onload=function(){setTimeout(function(){window.print();setTimeout(function(){window.close();},500);},250);};<\/script>
  1971.     </body></html>`);
  1972.     win.document.close();
  1973. }
  1974. /* ============================================================
  1975.    PANIER MOBILE (tiroir)
  1976.    ============================================================ */
  1977. function openCartDrawer() {
  1978.     document.getElementById('posCart').classList.add('mobile-open');
  1979.     document.getElementById('cartBackdrop').classList.add('open');
  1980. }
  1981. function closeCartDrawer() {
  1982.     document.getElementById('posCart').classList.remove('mobile-open');
  1983.     document.getElementById('cartBackdrop').classList.remove('open');
  1984. }
  1985. document.getElementById('cartFab')?.addEventListener('click', openCartDrawer);
  1986. document.getElementById('cartCloseBtn')?.addEventListener('click', closeCartDrawer);
  1987. document.getElementById('cartBackdrop')?.addEventListener('click', closeCartDrawer);
  1988. /* ============================================================
  1989.    MODALS UTILITAIRES
  1990.    ============================================================ */
  1991. function openModal(id) {
  1992.     const el = document.getElementById(id);
  1993.     if (el) el.classList.add('open');
  1994. }
  1995. function closeModal(id) {
  1996.     const el = document.getElementById(id);
  1997.     if (el) el.classList.remove('open');
  1998. }
  1999. // Fermer en cliquant sur le fond de l'overlay
  2000. document.querySelectorAll('.pos-modal-overlay').forEach(overlay => {
  2001.     overlay.addEventListener('click', function (e) {
  2002.         if (e.target === this) closeModal(this.id);
  2003.     });
  2004. });
  2005. // Client badge → ouvre le modal de sélection
  2006. document.getElementById('customerBtn').addEventListener('click', function(e) {
  2007.     e.stopPropagation();
  2008.     openModal('customerModal');
  2009. });
  2010. // Bouton ajout client → ouvre le modal nouveau client
  2011. document.getElementById('addClientBtn')?.addEventListener('click', function(e) {
  2012.     e.stopPropagation();
  2013.     openModal('addClientModal');
  2014. });
  2015. // Sélection d'un client — listener direct sur chaque <li>
  2016. document.querySelectorAll('#customerModal .client-item').forEach(function(item) {
  2017.     item.addEventListener('click', function(e) {
  2018.         e.stopPropagation();
  2019.         const id      = this.dataset.clientId;
  2020.         const name    = this.dataset.clientName || this.querySelector('span').textContent.trim();
  2021.         const initial = name.charAt(0).toUpperCase();
  2022.         const f = { selectedClientId: id, selectedespeceClientId: id, selectedClientIdTicket: id };
  2023.         Object.entries(f).forEach(([fid, val]) => { const el = document.getElementById(fid); if (el) el.value = val; });
  2024.         const nd = document.getElementById('clientNameDisplay');
  2025.         const ad = document.getElementById('clientAvatarText');
  2026.         if (nd) nd.textContent = name;
  2027.         if (ad) ad.textContent = initial;
  2028.         closeModal('customerModal');
  2029.     });
  2030. });
  2031. /* ============================================================
  2032.    VIDER LE PANIER
  2033.    ============================================================ */
  2034. function confirmClearCart() {
  2035.     if (confirm('Vider complètement le panier ? Cette action est irréversible.')) {
  2036.         document.getElementById('clearCartForm').submit();
  2037.     }
  2038. }
  2039. /* ============================================================
  2040.    RECHERCHE & SCAN CODE-BARRES
  2041.    ============================================================ */
  2042. function isBarcodeLike(v) {
  2043.     return /^\d{8,13}$/.test(v);
  2044. }
  2045. function scanBarcodeAndAddToCart(code) {
  2046.     if (!CAISSE_SESSION_OUVERTE) {
  2047.         openModal('noCaisseSessionModal');
  2048.         return;
  2049.     }
  2050.     playCaisseBeep();
  2051.     document.getElementById('scanBarcodeInput').value = code;
  2052.     document.getElementById('scanBarcodeForm').submit();
  2053. }
  2054. document.addEventListener('DOMContentLoaded', function () {
  2055.     const searchInput = document.getElementById('searchInput');
  2056.     if (!searchInput) return;
  2057.     searchInput.focus();
  2058.     searchInput.addEventListener('click', function () { this.select(); });
  2059.     searchInput.addEventListener('keypress', function (e) {
  2060.         if (e.key === 'Enter') {
  2061.             e.preventDefault();
  2062.             const v = this.value.trim();
  2063.             if (isBarcodeLike(v)) {
  2064.                 scanBarcodeAndAddToCart(v);
  2065.             } else {
  2066.                 this.form.submit();
  2067.             }
  2068.         }
  2069.     });
  2070.     let scanTimeout;
  2071.     searchInput.addEventListener('input', function (e) {
  2072.         const v = this.value.trim();
  2073.         // Code-barres tapé/scanné : dès que la frappe s'arrête, on l'ajoute
  2074.         // directement au panier (comportement d'un vrai scanner de caisse).
  2075.         if (isBarcodeLike(v)) {
  2076.             clearTimeout(scanTimeout);
  2077.             scanTimeout = setTimeout(() => {
  2078.                 if (this.value.trim() === v) scanBarcodeAndAddToCart(v);
  2079.             }, 150);
  2080.             return;
  2081.         }
  2082.         // Certains scanners "collent" le code d'un coup (paste-like input) :
  2083.         // même logique, mais on retombe sur la recherche classique si ce
  2084.         // n'est finalement pas un code-barres (ex: texte collé).
  2085.         if (e.inputType === 'insertReplacementText' || e.inputType === 'insertFromPaste') {
  2086.             clearTimeout(scanTimeout);
  2087.             scanTimeout = setTimeout(() => {
  2088.                 if (this.value.trim() !== v) return;
  2089.                 if (isBarcodeLike(v)) {
  2090.                     scanBarcodeAndAddToCart(v);
  2091.                 } else {
  2092.                     this.form.submit();
  2093.                 }
  2094.             }, 50);
  2095.         }
  2096.     });
  2097. });
  2098. /* ============================================================
  2099.    FORMULAIRES DE REMISE (AJAX)
  2100.    ============================================================ */
  2101. document.addEventListener('DOMContentLoaded', function () {
  2102.     document.querySelectorAll('.discount-form').forEach(form => {
  2103.         form.addEventListener('submit', function (e) {
  2104.             e.preventDefault();
  2105.             fetch(this.action, {
  2106.                 method: 'POST',
  2107.                 body: new FormData(this),
  2108.                 headers: { 'X-Requested-With': 'XMLHttpRequest' }
  2109.             })
  2110.             .then(r => r.json())
  2111.             .then(data => { if (data.success) window.location.reload(); })
  2112.             .catch(() => this.submit());
  2113.         });
  2114.     });
  2115. });
  2116. /* ============================================================
  2117.    CHANGEMENT DE SALARIÉ / DÉCONNEXION
  2118.    ============================================================ */
  2119. document.addEventListener('DOMContentLoaded', function () {
  2120.     var logoutBtn = document.getElementById('shiftLogoutBtn');
  2121.     if (!logoutBtn) return;
  2122.     logoutBtn.addEventListener('click', function (e) {
  2123.         var panierCount = parseInt(this.dataset.panierCount || '0', 10);
  2124.         var message = panierCount > 0
  2125.             ? 'Le panier en cours contient ' + panierCount + ' article(s) non validé(s) qui seront perdus. Se déconnecter maintenant ?'
  2126.             : 'Terminer votre service et laisser la place au salarié suivant ?';
  2127.         if (!window.confirm(message)) {
  2128.             e.preventDefault();
  2129.         }
  2130.     });
  2131. });
  2132. </script>
  2133. {% endblock %}