Added support for discounts in the POS checkout process, allowing operators to apply either a fixed amount or a percentage off the gross total. Updated the state management to include discount details and modified the receipt and cart rendering to display pre- and post-discount values. Enhanced the checkout API to handle discount inputs and ensure proper calculations. Updated relevant tests to cover new discount logic.
538 lines
13 KiB
CSS
538 lines
13 KiB
CSS
@import "tailwindcss";
|
|
@import "tw-animate-css";
|
|
@import "shadcn/tailwind.css";
|
|
@import "../styles/themes.css";
|
|
|
|
@custom-variant dark (&:is(.dark *));
|
|
|
|
@theme inline {
|
|
--color-background: var(--background);
|
|
--color-foreground: var(--foreground);
|
|
--font-sans: var(--font-body);
|
|
--font-mono: var(--font-geist-mono);
|
|
--font-heading: var(--font-heading);
|
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
--color-sidebar-border: var(--sidebar-border);
|
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
--color-sidebar-accent: var(--sidebar-accent);
|
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
--color-sidebar-primary: var(--sidebar-primary);
|
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
--color-sidebar: var(--sidebar);
|
|
--color-chart-5: var(--chart-5);
|
|
--color-chart-4: var(--chart-4);
|
|
--color-chart-3: var(--chart-3);
|
|
--color-chart-2: var(--chart-2);
|
|
--color-chart-1: var(--chart-1);
|
|
--color-ring: var(--ring);
|
|
--color-input-highlight: var(--input-highlight);
|
|
--color-input: var(--input);
|
|
--color-border: var(--border);
|
|
--color-destructive: var(--destructive);
|
|
--color-success: var(--success);
|
|
--color-success-foreground: var(--success-foreground);
|
|
--color-warn: var(--warn);
|
|
--color-warn-foreground: var(--warn-foreground);
|
|
--color-info: var(--info);
|
|
--color-info-foreground: var(--info-foreground);
|
|
--color-accent-foreground: var(--accent-foreground);
|
|
--color-accent: var(--accent);
|
|
--color-muted-foreground: var(--muted-foreground);
|
|
--color-muted: var(--muted);
|
|
--color-secondary-foreground: var(--secondary-foreground);
|
|
--color-secondary: var(--secondary);
|
|
--color-primary-foreground: var(--primary-foreground);
|
|
--color-primary: var(--primary);
|
|
--color-popover-foreground: var(--popover-foreground);
|
|
--color-popover: var(--popover);
|
|
--color-card-foreground: var(--card-foreground);
|
|
--color-card: var(--card);
|
|
--radius-sm: calc(var(--radius) * 0.6);
|
|
--radius-md: calc(var(--radius) * 0.8);
|
|
--radius-lg: var(--radius);
|
|
--radius-xl: calc(var(--radius) * 1.4);
|
|
--radius-2xl: calc(var(--radius) * 1.8);
|
|
--radius-3xl: calc(var(--radius) * 2.2);
|
|
--radius-4xl: calc(var(--radius) * 2.6);
|
|
}
|
|
|
|
@layer base {
|
|
* {
|
|
@apply border-border outline-ring/50;
|
|
}
|
|
body {
|
|
@apply bg-background text-foreground;
|
|
}
|
|
html {
|
|
@apply font-sans;
|
|
}
|
|
h1, h2, h3, h4, h5, h6 {
|
|
font-family: var(--font-heading);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
body {
|
|
font-family: var(--font-body);
|
|
}
|
|
}
|
|
|
|
/* ============================================================================
|
|
* Map labels — clean rounded pills on the supplier map. Token-driven so
|
|
* every theme picks up its sidebar/card colours automatically.
|
|
* ========================================================================= */
|
|
.supplier-map-label {
|
|
border: 0;
|
|
border-radius: 999px;
|
|
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.16);
|
|
background: var(--card);
|
|
color: var(--foreground);
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
padding: 3px 7px;
|
|
}
|
|
|
|
.supplier-map-label::before {
|
|
display: none;
|
|
}
|
|
|
|
.dark .supplier-map-label {
|
|
background: oklch(0.20 0.01 240);
|
|
color: oklch(0.94 0.005 240);
|
|
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.34);
|
|
}
|
|
|
|
.leaflet-container {
|
|
z-index: 0;
|
|
}
|
|
|
|
.leaflet-pane,
|
|
.leaflet-top,
|
|
.leaflet-bottom {
|
|
z-index: 10;
|
|
}
|
|
|
|
.dark .leaflet-control-zoom a,
|
|
.dark .leaflet-control-attribution,
|
|
.dark .leaflet-popup-content-wrapper,
|
|
.dark .leaflet-popup-tip {
|
|
background: var(--card);
|
|
color: var(--card-foreground);
|
|
}
|
|
|
|
.dark .leaflet-control-zoom a {
|
|
border-color: var(--border);
|
|
}
|
|
|
|
.dark .leaflet-control-attribution a,
|
|
.dark .leaflet-popup-content a {
|
|
color: var(--primary);
|
|
}
|
|
|
|
/* ============================================================================
|
|
* Modal shell — single source of truth for dialog/sheet padding rhythm.
|
|
* Every dialog in the app composes this so they all line up.
|
|
* ========================================================================= */
|
|
.modal-shell {
|
|
@apply flex max-h-[90vh] flex-col gap-0 overflow-hidden bg-card text-card-foreground;
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
.modal-shell__header {
|
|
@apply flex flex-row items-center justify-between gap-3 border-b px-6 py-4;
|
|
}
|
|
.modal-shell__title {
|
|
@apply truncate text-base font-semibold tracking-tight;
|
|
}
|
|
.modal-shell__body {
|
|
@apply flex-1 overflow-y-auto px-6 py-5;
|
|
}
|
|
.modal-shell__footer {
|
|
@apply flex flex-row flex-wrap items-center justify-end gap-2 border-t px-6 pt-3 pb-5;
|
|
}
|
|
.modal-shell__footer--split {
|
|
@apply justify-between;
|
|
}
|
|
|
|
/* ============================================================================
|
|
* Shimmer + dashboard polish — keyframes used by the dashboard skeleton
|
|
* and card-mount fade-in. Hardware-accelerated (transform/opacity only).
|
|
* ========================================================================= */
|
|
@keyframes shimmer {
|
|
0% { background-position: 200% 0; }
|
|
100% { background-position: -200% 0; }
|
|
}
|
|
|
|
@keyframes dashboard-fade-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(6px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.dashboard-row {
|
|
animation: dashboard-fade-in 360ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
}
|
|
|
|
/* ============================================================================
|
|
* KPI delta flash — one-shot when a metric's value changes (Customer count,
|
|
* Orders count). Tracked by useRef in KpiCard so the first render doesn't
|
|
* fire it; only subsequent changes do. The chip is keyed on the change
|
|
* counter so React remounts the element and retriggers the animation.
|
|
* ========================================================================= */
|
|
@keyframes kpi-delta-flash-up {
|
|
0% {
|
|
background: transparent;
|
|
transform: scale(1);
|
|
}
|
|
20% {
|
|
background: color-mix(in oklch, oklch(0.62 0.16 155) 22%, transparent);
|
|
transform: scale(1.06);
|
|
}
|
|
100% {
|
|
background: transparent;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes kpi-delta-flash-down {
|
|
0% {
|
|
background: transparent;
|
|
transform: scale(1);
|
|
}
|
|
20% {
|
|
background: color-mix(in oklch, oklch(0.62 0.21 25) 22%, transparent);
|
|
transform: scale(1.06);
|
|
}
|
|
100% {
|
|
background: transparent;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
.kpi-delta-up {
|
|
animation: kpi-delta-flash-up 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.kpi-delta-down {
|
|
animation: kpi-delta-flash-down 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
/* ============================================================================
|
|
* Activity pulse halo — the literal "pulse" the chart name promises.
|
|
* A stroked circle that breathes outward from each series' endpoint dot.
|
|
* Staggered per series via inline `animationDelay` so the three lines
|
|
* pulse on their own beat instead of in unison. Infinite loop.
|
|
* `r` is animated directly on the SVG circle (Chrome 77+, FF 70+).
|
|
* ========================================================================= */
|
|
@keyframes activity-pulse-halo {
|
|
0% {
|
|
r: 5;
|
|
opacity: 0.85;
|
|
stroke-width: 2;
|
|
}
|
|
100% {
|
|
r: 20;
|
|
opacity: 0;
|
|
stroke-width: 0.5;
|
|
}
|
|
}
|
|
|
|
.activity-pulse-halo {
|
|
animation: activity-pulse-halo 2.2s cubic-bezier(0, 0.55, 0.45, 1) infinite;
|
|
}
|
|
|
|
/* Honor user preference for reduced motion */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.dashboard-row,
|
|
[style*="shimmer"],
|
|
.kpi-delta-up,
|
|
.kpi-delta-down,
|
|
.activity-pulse-halo {
|
|
animation: none !important;
|
|
}
|
|
}
|
|
|
|
/* ============================================================================
|
|
* WFDASH brand-stripe utility
|
|
*
|
|
* Reusable tri-color accent for the WFDASH brand (blue → orange → red).
|
|
* Used by the sidebar signature, the page-top banner, and the login card.
|
|
* Resolves to the `var(--brand-*)` tokens defined per-theme, so it
|
|
* degrades gracefully to a single primary on non-WFDASH themes (where
|
|
* the tokens fall back to the nearest semantic analogue).
|
|
* ========================================================================= */
|
|
.wfdash-brand-stripe {
|
|
background: linear-gradient(
|
|
90deg,
|
|
var(--brand-blue, var(--primary)) 0%,
|
|
var(--brand-blue, var(--primary)) 28%,
|
|
var(--brand-orange, var(--warn)) 50%,
|
|
var(--brand-red, var(--destructive)) 72%,
|
|
var(--brand-red, var(--destructive)) 100%
|
|
);
|
|
}
|
|
|
|
/* ============================================================================
|
|
* SOP print styles
|
|
*
|
|
* Used by the /sops/<slug>/print routes to render the markdown source of
|
|
* the SOPs as a clean A4 / Letter document. The screen view keeps the
|
|
* full app chrome (nav, brand stripe, theme); the print view below
|
|
* strips it down to a single-column document with a small page header /
|
|
* footer and a trigger to call `window.print()` on load.
|
|
*
|
|
* The browser's "Save as PDF" print option converts this view to a PDF
|
|
* file. The page is also readable on screen as a preview before printing.
|
|
* ========================================================================= */
|
|
|
|
.sop-print {
|
|
/* Black on white, single column, A4-friendly. The screen view is
|
|
unaffected — the print view is its own route. */
|
|
color: #111;
|
|
background: #fff;
|
|
max-width: 720px;
|
|
margin: 0 auto;
|
|
padding: 24px 32px;
|
|
font-size: 11pt;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.sop-print-header {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 2px solid #111;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.sop-print-header h1 {
|
|
font-size: 18pt;
|
|
font-weight: 700;
|
|
margin: 0;
|
|
}
|
|
|
|
.sop-print-header .sop-print-meta {
|
|
text-align: right;
|
|
font-size: 9pt;
|
|
color: #555;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.sop-print .sop-section-break {
|
|
/* Each "## 1. ...", "## 2. ..." section starts on a new page. The
|
|
first section omits this so we don't waste a page. */
|
|
page-break-before: always;
|
|
break-before: page;
|
|
padding-top: 12px;
|
|
border-top: 1px solid #ccc;
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.sop-print h2 {
|
|
font-size: 14pt;
|
|
font-weight: 700;
|
|
margin: 0 0 12px 0;
|
|
page-break-after: avoid;
|
|
break-after: avoid;
|
|
}
|
|
|
|
.sop-print h3 {
|
|
font-size: 12pt;
|
|
font-weight: 600;
|
|
margin: 16px 0 6px 0;
|
|
page-break-after: avoid;
|
|
break-after: avoid;
|
|
}
|
|
|
|
.sop-print h4 {
|
|
font-size: 11pt;
|
|
font-weight: 600;
|
|
margin: 12px 0 4px 0;
|
|
page-break-after: avoid;
|
|
break-after: avoid;
|
|
}
|
|
|
|
.sop-print p,
|
|
.sop-print li {
|
|
margin: 6px 0;
|
|
orphans: 3;
|
|
widows: 3;
|
|
}
|
|
|
|
.sop-print ul,
|
|
.sop-print ol {
|
|
margin: 6px 0 6px 24px;
|
|
padding: 0;
|
|
}
|
|
|
|
.sop-print li {
|
|
margin: 3px 0;
|
|
}
|
|
|
|
.sop-print table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 12px 0;
|
|
font-size: 9.5pt;
|
|
page-break-inside: avoid;
|
|
break-inside: avoid;
|
|
}
|
|
|
|
.sop-print thead {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.sop-print th,
|
|
.sop-print td {
|
|
border: 1px solid #ccc;
|
|
padding: 4px 8px;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
}
|
|
|
|
.sop-print th {
|
|
font-weight: 600;
|
|
font-size: 9pt;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.sop-print code {
|
|
background: #f4f4f4;
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.sop-print pre {
|
|
background: #f4f4f4;
|
|
border: 1px solid #ddd;
|
|
padding: 8px 10px;
|
|
border-radius: 4px;
|
|
overflow-x: auto;
|
|
font-size: 9pt;
|
|
page-break-inside: avoid;
|
|
break-inside: avoid;
|
|
}
|
|
|
|
.sop-print pre code {
|
|
background: transparent;
|
|
padding: 0;
|
|
}
|
|
|
|
.sop-print blockquote {
|
|
border-left: 3px solid #888;
|
|
padding: 4px 12px;
|
|
margin: 8px 0;
|
|
color: #444;
|
|
font-style: italic;
|
|
background: #fafafa;
|
|
}
|
|
|
|
.sop-print hr {
|
|
border: none;
|
|
border-top: 1px solid #ccc;
|
|
margin: 16px 0;
|
|
}
|
|
|
|
.sop-print a {
|
|
color: #0645ad;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.sop-print-toc {
|
|
margin: 0 0 32px 0;
|
|
padding: 12px 16px;
|
|
background: #f8f8f8;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 4px;
|
|
page-break-after: always;
|
|
break-after: page;
|
|
}
|
|
|
|
.sop-print-toc h2 {
|
|
font-size: 12pt;
|
|
margin: 0 0 8px 0;
|
|
}
|
|
|
|
.sop-print-toc ol {
|
|
margin: 0;
|
|
padding-left: 20px;
|
|
font-size: 10pt;
|
|
}
|
|
|
|
.sop-print-toc li {
|
|
margin: 3px 0;
|
|
}
|
|
|
|
.sop-print-actions {
|
|
/* The "Print this page" button is shown on screen but hidden in
|
|
@media print so it doesn't end up in the PDF. */
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
padding: 8px 12px;
|
|
background: #f8f8f8;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 4px;
|
|
font-size: 10pt;
|
|
color: #555;
|
|
}
|
|
|
|
.sop-print-actions button {
|
|
padding: 4px 10px;
|
|
background: #111;
|
|
color: #fff;
|
|
border: 0;
|
|
border-radius: 3px;
|
|
font-size: 10pt;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.sop-print-actions button:hover {
|
|
background: #333;
|
|
}
|
|
|
|
.sop-print-footer {
|
|
margin-top: 24px;
|
|
padding-top: 8px;
|
|
border-top: 1px solid #ccc;
|
|
font-size: 8pt;
|
|
color: #777;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
}
|
|
|
|
@media print {
|
|
/* Hide app chrome (sidebar, brand stripe, top nav) when the
|
|
user prints from any /sops/<slug>/print route. The .sop-print
|
|
root keeps everything else. */
|
|
body:has(.sop-print) [data-slot="sidebar"],
|
|
body:has(.sop-print) [data-slot="brand-stripe"],
|
|
body:has(.sop-print) header,
|
|
body:has(.sop-print) nav {
|
|
display: none !important;
|
|
}
|
|
|
|
body:has(.sop-print) {
|
|
background: #fff !important;
|
|
}
|
|
|
|
/* Hide the on-screen action bar. */
|
|
.sop-print-actions {
|
|
display: none !important;
|
|
}
|
|
|
|
@page {
|
|
size: A4;
|
|
margin: 18mm 16mm 18mm 16mm;
|
|
}
|
|
}
|