diff --git a/addons/laundry_management/static/src/scss/laundry_pos.scss b/addons/laundry_management/static/src/scss/laundry_pos.scss new file mode 100644 index 0000000..3d06e60 --- /dev/null +++ b/addons/laundry_management/static/src/scss/laundry_pos.scss @@ -0,0 +1,994 @@ +// ============================================================================ +// Laundry Management — POS / Backend Design System +// 8px grid, soft shadows, premium pill components. +// ============================================================================ + +// ── Tokens ───────────────────────────────────────────────────────────────── +$lm-space-1: 4px; +$lm-space-2: 8px; +$lm-space-3: 12px; +$lm-space-4: 16px; +$lm-space-5: 24px; + +$lm-radius-sm: 6px; +$lm-radius-md: 10px; +$lm-radius-lg: 14px; + +$lm-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 1px rgba(15, 23, 42, 0.04); +$lm-shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08), 0 2px 4px rgba(15, 23, 42, 0.05); +$lm-shadow-lg: 0 10px 30px rgba(15, 23, 42, 0.12), 0 4px 8px rgba(15, 23, 42, 0.06); + +$lm-color-urgent: #EF4444; +$lm-color-delivery: #10B981; +$lm-color-vip: #8B5CF6; +$lm-color-normal: #6B7280; +$lm-color-type: #4F46E5; + +$lm-font-xs: 11px; +$lm-font-sm: 13px; +$lm-font-md: 14px; +$lm-font-lg: 16px; +$lm-font-xl: 18px; + +$lm-bg-card: #FFFFFF; +$lm-bg-soft: #F8FAFC; +$lm-border: #E2E8F0; +$lm-text: #0F172A; +$lm-text-muted: #64748B; + +// ── Pill component (shared across panel / kanban / receipt) ─────────────── +.laundry-pill { + display: inline-flex; + align-items: center; + padding: $lm-space-1 $lm-space-3; + border-radius: 9999px; + font-size: $lm-font-sm; + font-weight: 600; + line-height: 1.2; + color: #fff; + background-color: $lm-color-normal; + box-shadow: $lm-shadow-sm; + white-space: nowrap; + transition: transform 0.12s ease, box-shadow 0.12s ease; + + &:hover { transform: translateY(-1px); box-shadow: $lm-shadow-md; } + + .fa { font-size: 0.95em; } + + &--type { background-color: $lm-color-type; } + &--attr { background-color: $lm-color-normal; } + &--urgent { background-color: $lm-color-urgent; } + &--delivery { background-color: $lm-color-delivery; } + &--vip { background-color: $lm-color-vip; } + &--deferred { background-color: #F59E0B; } + + // Semantic data overrides — win over default --attr + &[data-priority="urgent"] { background-color: $lm-color-urgent; } + &[data-delivery="1"] { background-color: $lm-color-delivery; } +} + +// ── Settle Due Banner (POS Mode indicator) ──────────────────────────────── +.laundry-settle-banner { + margin: $lm-space-2 $lm-space-3 0 $lm-space-3; + padding: $lm-space-3 $lm-space-4; + background: linear-gradient(135deg, #F59E0B 0%, #F97316 100%); + color: #fff; + border-radius: $lm-radius-md; + box-shadow: $lm-shadow-md; + display: flex; + align-items: center; + justify-content: space-between; + gap: $lm-space-3; + animation: laundry-settle-banner-pulse 2.4s ease-in-out infinite; + + &__lead { + display: flex; + align-items: center; + gap: $lm-space-3; + min-width: 0; + } + + &__icon { + font-size: $lm-font-xl; + flex-shrink: 0; + } + + &__text { + display: flex; + flex-direction: column; + min-width: 0; + } + + &__title { + font-size: $lm-font-md; + font-weight: 800; + letter-spacing: 0.06em; + line-height: 1.15; + text-transform: uppercase; + } + + &__meta { + display: flex; + align-items: baseline; + gap: $lm-space-3; + margin-top: 3px; + min-width: 0; + } + + &__partner { + font-size: $lm-font-sm; + font-weight: 600; + opacity: 0.95; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 40ch; + } + + &__amount { + font-size: $lm-font-md; + font-weight: 800; + letter-spacing: 0.01em; + padding: 1px 8px; + background: rgba(255, 255, 255, 0.2); + border-radius: $lm-radius-sm; + white-space: nowrap; + } + + &__hint { + font-size: $lm-font-xs; + opacity: 0.85; + margin-top: 2px; + font-style: italic; + } + + &__exit { + background: rgba(255, 255, 255, 0.18); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.4); + padding: $lm-space-2 $lm-space-3; + border-radius: $lm-radius-sm; + font-size: $lm-font-sm; + font-weight: 600; + cursor: pointer; + flex-shrink: 0; + transition: background 0.15s ease, transform 0.12s ease; + + &:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-1px); } + &:active { transform: translateY(0); } + &:focus-visible { + outline: 2px solid #fff; + outline-offset: 2px; + } + } +} + +@keyframes laundry-settle-banner-pulse { + 0%, 100% { box-shadow: 0 4px 12px rgba(245, 158, 11, 0.35); } + 50% { box-shadow: 0 4px 20px rgba(245, 158, 11, 0.65); } +} + +// ── Global lock of POS chrome while settle-due mode is active ───────────── +// The banner component toggles `pos-laundry-settle-active` on
. +// CSS is a clarity layer, not the safety layer — JS (pos_store_patch / +// order_tabs_patch / navbar_patch) is the real gate. But here we also +// disable pointer-events on the peripherals because SINGLE-ORDER MODE +// guarantees the only tab is the settle order itself; everything else +// is visual chrome that shouldn't respond to clicks. +body.pos-laundry-settle-active { + // Every non-active tab (should be zero under single-order mode — + // but defense in depth): hatched, dim, unclickable. + .floating-order-container .btn:not(.active) { + pointer-events: none; + opacity: 0.35; + filter: saturate(0.3); + position: relative; + + &::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: repeating-linear-gradient( + 135deg, + rgba(245, 158, 11, 0.0) 0 6px, + rgba(245, 158, 11, 0.15) 6px 12px + ); + pointer-events: none; + } + } + + // Active settle tab — keep visible & highlighted in warning orange. + .floating-order-container .btn.active { + outline: 2px solid #F59E0B; + outline-offset: 2px; + pointer-events: none; // the tab IS the current order; no switch needed + } + + // "+" new-order button inside ListContainer — hard-disabled visually. + // JS addNewOrder patch also blocks it. + .list-container-add, + .o_list_container_add, + button[title*="Add a new order" i], + button[aria-label*="Add a new order" i] { + pointer-events: none; + opacity: 0.35; + filter: saturate(0.3); + } +} + +// ── Order Context Panel (POS right-side card) ───────────────────────────── +.laundry-context-panel { + margin: $lm-space-2 $lm-space-3; + padding: $lm-space-3 $lm-space-4; + background: $lm-bg-card; + border: 1px solid $lm-border; + border-radius: $lm-radius-md; + box-shadow: $lm-shadow-sm; + display: flex; + flex-direction: column; + gap: $lm-space-2; + transition: box-shadow 0.18s ease, transform 0.18s ease; + + &:hover { box-shadow: $lm-shadow-md; } + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + } + + &__title { + font-size: $lm-font-xs; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: $lm-text-muted; + } + + &__edit { + background: transparent; + border: 0; + color: $lm-text-muted; + padding: $lm-space-1 $lm-space-2; + border-radius: $lm-radius-sm; + cursor: pointer; + transition: background 0.12s ease, color 0.12s ease; + + &:hover { background: $lm-bg-soft; color: $lm-text; } + &:focus-visible { + outline: 2px solid rgba(99, 102, 241, 0.45); + outline-offset: 2px; + } + } + + &__row { + display: flex; + flex-wrap: wrap; + gap: $lm-space-2; + } + + &__attrs .laundry-pill { font-size: $lm-font-xs; padding: 2px $lm-space-2; } + + &__empty { + font-size: $lm-font-sm; + color: $lm-text-muted; + background: $lm-bg-soft; + padding: $lm-space-2 $lm-space-3; + border-radius: $lm-radius-sm; + text-align: center; + } + + &__cta { + display: flex; + align-items: center; + justify-content: center; + gap: $lm-space-2; + width: 100%; + padding: $lm-space-3 $lm-space-4; + background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%); + color: #fff; + font-size: $lm-font-md; + font-weight: 600; + border: 0; + border-radius: $lm-radius-md; + box-shadow: $lm-shadow-sm; + cursor: pointer; + transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease; + + .fa { font-size: 1.05em; } + + &:hover { + transform: translateY(-1px); + box-shadow: $lm-shadow-md; + filter: brightness(1.05); + } + &:active { transform: translateY(0); filter: brightness(0.95); } + &:focus-visible { + outline: 2px solid rgba(99, 102, 241, 0.55); + outline-offset: 2px; + } + } + + &__edit-label { + margin-inline-start: $lm-space-1; + font-size: $lm-font-xs; + font-weight: 600; + } + + &__delivery { + display: flex; + flex-direction: column; + gap: $lm-space-1; + padding-top: $lm-space-2; + border-top: 1px dashed $lm-border; + } + + &__delivery-row { + display: flex; + align-items: center; + gap: $lm-space-2; + font-size: $lm-font-sm; + color: $lm-text; + + .fa { color: $lm-color-delivery; width: 14px; text-align: center; } + } + + &[data-empty="1"] { background: $lm-bg-soft; } + &[data-delivery="1"] { border-left: 3px solid $lm-color-delivery; } +} + +// ── Popup polish (shared by all 3 laundry popups) ───────────────────────── +.modal .modal-dialog { + .btn.btn-outline-primary, + .btn.btn-primary { + border-radius: $lm-radius-md; + transition: transform 0.12s ease, box-shadow 0.12s ease, + background-color 0.12s ease, border-color 0.12s ease; + } + + .btn.btn-primary { + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18); + } + + .btn.btn-outline-primary:hover { + transform: translateY(-1px); + box-shadow: $lm-shadow-md; + } + + .modal-footer .btn-primary, + .modal-footer .btn-secondary { + position: sticky; + bottom: 0; + } +} + +// ── Receipt details (printer-friendly) ──────────────────────────────────── +.laundry-receipt-details { + margin-top: $lm-space-2; + text-align: center; + + &__sep { + border-top: 1px dashed #000; + margin: $lm-space-2 0; + } + + &__title { + font-weight: 700; + font-size: $lm-font-md; + margin-bottom: $lm-space-1; + letter-spacing: 0.04em; + } + + &__row { + display: flex; + justify-content: space-between; + gap: $lm-space-3; + font-size: $lm-font-sm; + margin: 2px 0; + } + + &__label { + font-weight: 600; + color: #000; + min-width: 80px; + text-align: start; + } + + &__value { + text-align: end; + flex: 1; + } + + &__chip { + display: inline-block; + padding: 0 $lm-space-2; + margin: 0 2px; + border: 1px solid #000; + border-radius: $lm-radius-sm; + font-size: $lm-font-xs; + } +} + +// RTL safety +[dir="rtl"] .laundry-receipt-details__label { text-align: end; } +[dir="rtl"] .laundry-receipt-details__value { text-align: start; } + +// ── Operational Control Board (laundry.order kanban) ────────────────────── +.laundry-board { + .laundry-board__card { + position: relative; + background: $lm-bg-card; + border: 1px solid $lm-border; + border-radius: $lm-radius-md; + box-shadow: $lm-shadow-sm; + padding: $lm-space-3 $lm-space-4 $lm-space-3 ($lm-space-4 + 4px); + display: flex; + flex-direction: column; + gap: $lm-space-2; + transition: box-shadow 0.18s ease, transform 0.18s ease; + + &:hover { box-shadow: $lm-shadow-md; transform: translateY(-1px); } + } + + .laundry-board__strip { + position: absolute; + left: 0; top: 0; bottom: 0; + width: 4px; + border-top-left-radius: $lm-radius-md; + border-bottom-left-radius: $lm-radius-md; + background: $lm-color-normal; + } + + .laundry-board__card[data-state="intake"] .laundry-board__strip { background: #3B82F6; } + .laundry-board__card[data-state="processing"] .laundry-board__strip { background: #F59E0B; } + .laundry-board__card[data-state="ready"] .laundry-board__strip { background: $lm-color-delivery; } + .laundry-board__card[data-state="delivered"] .laundry-board__strip { background: $lm-color-normal; } + .laundry-board__card[data-priority="urgent"] .laundry-board__strip { background: $lm-color-urgent; } + .laundry-board__card[data-priority="urgent"] { border-color: rgba(239, 68, 68, 0.35); } + + .laundry-board__head { + display: flex; + flex-direction: column; + gap: 2px; + } + + .laundry-board__title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: $lm-space-2; + } + + .laundry-board__name { + font-size: $lm-font-md; + color: $lm-text; + } + + .laundry-board__total { + font-size: $lm-font-md; + font-weight: 700; + color: $lm-text; + } + + .laundry-board__customer { + font-size: $lm-font-sm; + color: $lm-text-muted; + display: flex; + align-items: center; + } + + .laundry-board__badges { + display: flex; + flex-wrap: wrap; + gap: $lm-space-1; + } + + .laundry-board__meta { + display: flex; + flex-wrap: wrap; + gap: $lm-space-3; + font-size: $lm-font-xs; + color: $lm-text-muted; + } + + .laundry-board__due { + color: $lm-color-urgent; + font-weight: 700; + } + + .laundry-board__actions { margin-top: $lm-space-1; } +} + + +// ── ProductConfiguratorPopup — laundry enhancement ───────────────────── +// Scoped exclusively under `.popup-product-configurator.laundry-enhanced`, +// applied to the modal-content via Dialog's `contentClass` prop from +// xml/product_configurator_popup.xml. Non-laundry configurator popups are +// unaffected. +.popup-product-configurator.laundry-enhanced { + + // Attribute group — stronger title, better spacing. + .modal-body .attribute { + margin-bottom: $lm-space-4 !important; + padding-bottom: $lm-space-3; + border-bottom: 1px dashed $lm-border; + + &:last-child { + margin-bottom: 0 !important; + padding-bottom: 0; + border-bottom: 0; + } + + .attribute_name { + font-size: $lm-font-sm; + font-weight: 800 !important; + text-transform: uppercase; + letter-spacing: 0.09em; + color: $lm-text-muted; + margin-bottom: $lm-space-3 !important; + } + } + + // Touch-first tiles — applies to both Radio and Pills renderers, + // which share the same `.configurator_radio > .attribute-name-cell` + // structure in core. + .configurator_radio { + > .d-flex { + flex-wrap: wrap !important; + gap: $lm-space-3 !important; + } + + .attribute-name-cell { + flex: 1 1 calc(33% - #{$lm-space-3}); + min-width: 140px; + margin: 0; + padding: 0; + + // Hide the native radio — the label IS the tile. + .form-check-input, + .radio-check { + position: absolute !important; + opacity: 0 !important; + pointer-events: none !important; + width: 0 !important; + height: 0 !important; + } + + > label { + display: flex !important; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: $lm-space-2; + width: 100%; + min-height: 64px; + padding: 12px 16px !important; + border: 2px solid $lm-border !important; + border-radius: 12px !important; + background: #fff !important; + color: $lm-text !important; + font-size: $lm-font-md; + font-weight: 700; + line-height: 1.25; + cursor: pointer; + position: relative; + box-shadow: none !important; + transition: transform 0.14s ease, + border-color 0.14s ease, + box-shadow 0.16s ease, + background 0.14s ease, + color 0.14s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: $lm-shadow-sm !important; + } + &:active { transform: translateY(0); } + &:focus-visible { + outline: 2px solid rgba(79, 70, 229, 0.45); + outline-offset: 2px; + } + + // Wrap text span so conflict styling still readable. + > span:first-child { word-break: break-word; } + } + } + + // Selected state: + // - Pills renderer: core toggles `.active` on the