Skip to content

Commit fb08bdc

Browse files
authored
Merge branch 'v3-test' into dashboard-v3
2 parents fe5ac08 + 4439775 commit fb08bdc

92 files changed

Lines changed: 5769 additions & 404 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-ury.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Deploy URY V3
2+
3+
on:
4+
push:
5+
branches:
6+
- v3-test
7+
8+
permissions: {}
9+
10+
jobs:
11+
deploy:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Configure SSH
16+
env:
17+
SSH_PRIVATE_KEY: ${{ secrets.URY_DEPLOY_SSH_KEY }}
18+
SSH_HOST: ${{ secrets.URY_DEPLOY_HOST }}
19+
SSH_PORT: ${{ secrets.URY_DEPLOY_PORT }}
20+
run: |
21+
mkdir -p ~/.ssh
22+
23+
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
24+
chmod 600 ~/.ssh/id_ed25519
25+
26+
ssh-keyscan -p "$SSH_PORT" -H "$SSH_HOST" >> ~/.ssh/known_hosts
27+
28+
- name: Deploy
29+
env:
30+
SSH_HOST: ${{ secrets.URY_DEPLOY_HOST }}
31+
SSH_USER: ${{ secrets.URY_DEPLOY_USER }}
32+
SSH_PORT: ${{ secrets.URY_DEPLOY_PORT }}
33+
run: |
34+
ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
35+
"/home/beta/deploy-ury.sh \
36+
'${GITHUB_REF_NAME}' \
37+
'${GITHUB_SHA}'"

frontend/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<html lang="{{ user_lang }}" dir="{{ 'rtl' if is_rtl else 'ltr' }}">
55
<head>
66
<meta charset="UTF-8" />
7-
<link rel="icon" href="/ury.ico" />
7+
<link rel="icon" href="/assets/ury/pos/ury.ico" />
88
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
99
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
1010
<title>URY</title>

frontend/src/components/layout/Sidebar.tsx

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,18 @@ interface NavItem {
2828
const NAV_ITEMS: NavItem[] = [
2929
{ label: 'Dashboard', path: '/dashboard', icon: LayoutDashboard },
3030
{ label: 'Branch', path: '/branch', icon: Building2 },
31-
{ label: 'URY Room', path: '/room', icon: Map },
32-
{ label: 'URY Table', path: '/table', icon: Grid3X3 },
33-
{ label: 'URY Menu', path: '/menu', icon: UtensilsCrossed },
31+
{ label: 'Room', path: '/room', icon: Map },
32+
{ label: 'Table', path: '/table', icon: Grid3X3 },
33+
{ label: 'Menu', path: '/menu', icon: UtensilsCrossed },
34+
];
35+
36+
const SETTINGS_ITEMS: NavItem[] = [
3437
{ label: 'POS Profile', path: '/pos-profile', icon: SlidersHorizontal },
3538
{ label: 'User', path: '/user', icon: Users },
36-
{ label: 'Aggregators', path: '/aggregator', icon: CreditCard }
39+
{ label: 'Branch', path: '/branch', icon: Building2 },
40+
{ label: 'Aggregators', path: '/aggregator', icon: Store },
41+
{ label: 'URY Report Settings', path: '/report-settings', icon: FileText },
42+
{ label: 'Production Unit', path: '/production-unit', icon: Grid }
3743
];
3844

3945
const reportLinkClass = ({ isActive }: { isActive: boolean }) =>
@@ -89,7 +95,7 @@ const ReportsPanel: React.FC = () => (
8995

9096
const MainPanel: React.FC<{ isManager: boolean }> = ({ isManager }) => {
9197
const location = useLocation();
92-
const isAdvancedPath = location.pathname.startsWith('/report-settings') || location.pathname.startsWith('/production-unit');
98+
const isAdvancedPath = SETTINGS_ITEMS.some((item) => location.pathname.startsWith(item.path));
9399
const [isAdvancedOpen, setIsAdvancedOpen] = useState<boolean>(isAdvancedPath);
94100

95101
return (
@@ -225,6 +231,47 @@ const MainPanel: React.FC<{ isManager: boolean }> = ({ isManager }) => {
225231
</NavLink>
226232
</div>
227233
)}
234+
<div className="pt-2">
235+
<button
236+
onClick={() => setIsAdvancedOpen(!isAdvancedOpen)}
237+
className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-lg text-sm font-medium transition-colors ${
238+
isAdvancedPath
239+
? 'text-[#2563eb] font-semibold bg-blue-50'
240+
: 'text-gray-600 hover:bg-blue-50 hover:text-[#2563eb]'
241+
}`}
242+
>
243+
<div className="flex items-center space-x-3">
244+
<Settings className="w-5 h-5 shrink-0" />
245+
<span>Settings</span>
246+
</div>
247+
<ChevronDown
248+
className={`w-4 h-4 transition-transform duration-200 ${
249+
isAdvancedOpen ? 'rotate-180 text-[#2563eb]' : 'text-gray-400'
250+
}`}
251+
/>
252+
</button>
253+
254+
{isAdvancedOpen && (
255+
<div className="mt-1 pl-4 space-y-1">
256+
{SETTINGS_ITEMS.map((item) => {
257+
const Icon = item.icon;
258+
return (
259+
<NavLink
260+
key={item.path}
261+
to={item.path}
262+
className={({ isActive }) =>
263+
`flex items-center space-x-3 px-3.5 py-2 rounded-lg text-xs font-medium transition-all ${
264+
isActive
265+
? 'bg-[#2563eb] text-white shadow-sm font-semibold'
266+
: 'text-gray-600 hover:bg-blue-50 hover:text-[#2563eb]'
267+
}`
268+
}
269+
>
270+
<Icon className="w-4 h-4 shrink-0" />
271+
<span>{item.label}</span>
272+
</NavLink>
273+
);
274+
})}
228275
</div>
229276
</div>
230277
</div>

frontend/src/pages/Dashboard/KPIGrid.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export const KPIGrid: React.FC<KPIGridProps> = ({ summary, loading }) => {
5858
/>
5959

6060
<KPICard
61-
title="Active Tables"
62-
value={`${totalTables} Tables`}
61+
title="Table Occupancy"
62+
value={`${occupancyRate}%`}
6363
loading={loading}
6464
/>
6565

packages/ui/src/components/dialog.tsx

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,113 @@ export interface DialogProps
7878
VariantProps<typeof dialogVariants> {
7979
open?: boolean
8080
onOpenChange?: (open: boolean) => void
81+
/** Whether pressing Escape closes the dialog. Defaults to true; set to
82+
* false for blocking gates (e.g. ChecklistGateDialog) that must not be
83+
* dismissible via Escape. */
84+
closeOnEscape?: boolean
85+
}
86+
87+
// Elements considered reachable via Tab, used both to seed initial focus and
88+
// to compute the wrap points for the focus trap.
89+
const FOCUSABLE_SELECTOR =
90+
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
91+
92+
const DialogTitleContext = React.createContext<string | undefined>(undefined)
93+
94+
function mergeRefs<T>(
95+
...refs: Array<React.Ref<T> | undefined>
96+
): (node: T | null) => void {
97+
return (node) => {
98+
refs.forEach((ref) => {
99+
if (!ref) return
100+
if (typeof ref === "function") ref(node)
101+
else (ref as React.MutableRefObject<T | null>).current = node
102+
})
103+
}
81104
}
82105

83106
const Dialog = React.forwardRef<HTMLDivElement, DialogProps>(
84-
({ className, variant, open, onOpenChange, children, ...props }, ref) => {
107+
(
108+
{ className, variant, open, onOpenChange, closeOnEscape = true, children, ...props },
109+
ref
110+
) => {
111+
const titleId = React.useId()
112+
const containerRef = React.useRef<HTMLDivElement | null>(null)
113+
const previousActiveElementRef = React.useRef<HTMLElement | null>(null)
114+
115+
// Remember what had focus before the dialog opened, lock body scroll
116+
// while it's open, move focus into the dialog, and restore both on
117+
// close/unmount.
118+
React.useEffect(() => {
119+
if (!open) return
120+
121+
previousActiveElementRef.current = document.activeElement as HTMLElement | null
122+
const previousOverflow = document.body.style.overflow
123+
document.body.style.overflow = "hidden"
124+
125+
const node = containerRef.current
126+
const focusable = node?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)
127+
;(focusable ?? node)?.focus()
128+
129+
return () => {
130+
document.body.style.overflow = previousOverflow
131+
previousActiveElementRef.current?.focus?.()
132+
}
133+
}, [open])
134+
135+
// Escape-to-close (the one place this is handled) and a basic Tab focus
136+
// trap that keeps focus cycling within the dialog.
137+
React.useEffect(() => {
138+
if (!open) return
139+
140+
function handleKeyDown(event: KeyboardEvent) {
141+
if (event.key === "Escape") {
142+
if (closeOnEscape) onOpenChange?.(false)
143+
return
144+
}
145+
146+
if (event.key === "Tab") {
147+
const node = containerRef.current
148+
if (!node) return
149+
const focusableEls = node.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)
150+
if (focusableEls.length === 0) {
151+
event.preventDefault()
152+
return
153+
}
154+
const first = focusableEls[0]
155+
const last = focusableEls[focusableEls.length - 1]
156+
if (event.shiftKey && document.activeElement === first) {
157+
event.preventDefault()
158+
last.focus()
159+
} else if (!event.shiftKey && document.activeElement === last) {
160+
event.preventDefault()
161+
first.focus()
162+
}
163+
}
164+
}
165+
166+
document.addEventListener("keydown", handleKeyDown)
167+
return () => document.removeEventListener("keydown", handleKeyDown)
168+
}, [open, closeOnEscape, onOpenChange])
169+
85170
if (!open) return null
86171

87172
return (
88173
<div
89-
ref={ref}
174+
ref={mergeRefs(ref, containerRef)}
175+
role="dialog"
176+
aria-modal="true"
177+
aria-labelledby={titleId}
90178
className={cn(dialogVariants({ variant, className }))}
91179
{...props}
92180
>
93181
<div
94182
className={cn(overlayVariants({ variant }))}
95183
onClick={() => onOpenChange?.(false)}
96184
/>
97-
{children}
185+
<DialogTitleContext.Provider value={titleId}>
186+
{children}
187+
</DialogTitleContext.Provider>
98188
</div>
99189
)
100190
}
@@ -165,13 +255,17 @@ DialogFooter.displayName = "DialogFooter"
165255
const DialogTitle = React.forwardRef<
166256
HTMLParagraphElement,
167257
React.HTMLAttributes<HTMLHeadingElement>
168-
>(({ className, ...props }, ref) => (
169-
<h2
170-
ref={ref}
171-
className={cn("text-lg font-semibold leading-tight tracking-tight", className)}
172-
{...props}
173-
/>
174-
))
258+
>(({ className, id, ...props }, ref) => {
259+
const contextTitleId = React.useContext(DialogTitleContext)
260+
return (
261+
<h2
262+
ref={ref}
263+
id={id ?? contextTitleId}
264+
className={cn("text-lg font-semibold leading-tight tracking-tight", className)}
265+
{...props}
266+
/>
267+
)
268+
})
175269
DialogTitle.displayName = "DialogTitle"
176270

177271
const DialogDescription = React.forwardRef<

packages/ui/tailwind-preset.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export default {
8787
// wider ambient one) so surfaces read as lifted rather than smudged.
8888
// Deliberately low-alpha: elevation should be felt, not seen.
8989
boxShadow: {
90+
xs: "0 1px 1px 0 hsl(var(--black) / 0.04)",
9091
sm: "0 1px 2px 0 hsl(var(--black) / 0.05)",
9192
DEFAULT:
9293
"0 1px 2px 0 hsl(var(--black) / 0.06), 0 1px 3px 0 hsl(var(--black) / 0.08)",

pos/src/components/ClosingPaymentTable.tsx

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,25 @@ import { ClosingPaymentSummary } from '../lib/pos-closing-api';
33
import { Input } from '@ury/ui';
44
import { formatCurrency } from '@ury/core';
55
import { cn } from '@ury/ui';
6+
import { t } from '../i18n';
67

78
interface ClosingPaymentTableProps {
89
rows: ClosingPaymentSummary[];
10+
/** Modes the cashier has explicitly entered a closing amount for (see Fix 2). */
11+
touchedModes: Set<string>;
912
onChange: (modeOfPayment: string, closingAmount: number) => void;
1013
}
1114

12-
const ClosingPaymentTable: React.FC<ClosingPaymentTableProps> = ({ rows, onChange }) => {
15+
const ClosingPaymentTable: React.FC<ClosingPaymentTableProps> = ({
16+
rows,
17+
touchedModes,
18+
onChange,
19+
}) => {
1320
const handleClosingAmountChange = (modeOfPayment: string, value: string) => {
14-
const closingAmount = parseFloat(value) || 0;
21+
const parsed = parseFloat(value);
22+
// Clamp to non-negative in JS -- the HTML `min="0"` attribute alone does
23+
// not stop programmatic or pasted negative input.
24+
const closingAmount = Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
1525
onChange(modeOfPayment, closingAmount);
1626
};
1727

@@ -27,25 +37,65 @@ const ClosingPaymentTable: React.FC<ClosingPaymentTableProps> = ({ rows, onChang
2737
Opening
2838
</th>
2939
<th className="text-right py-3 px-4 font-semibold text-gray-900">
30-
Expected
40+
<span
41+
title={t('pos_closing.help_expected')}
42+
className="inline-flex items-center gap-1 cursor-help"
43+
>
44+
Expected
45+
<span
46+
aria-hidden="true"
47+
className="inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-400 text-[10px] leading-none text-gray-500"
48+
>
49+
i
50+
</span>
51+
</span>
3152
</th>
3253
<th className="text-center py-3 px-4 font-semibold text-gray-900">
33-
Closing
54+
<span
55+
title={t('pos_closing.help_closing')}
56+
className="inline-flex items-center gap-1 cursor-help"
57+
>
58+
Closing
59+
<span
60+
aria-hidden="true"
61+
className="inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-400 text-[10px] leading-none text-gray-500"
62+
>
63+
i
64+
</span>
65+
</span>
3466
</th>
3567
<th className="text-right py-3 px-4 font-semibold text-gray-900">
36-
Difference
68+
<span
69+
title={t('pos_closing.help_difference')}
70+
className="inline-flex items-center gap-1 cursor-help"
71+
>
72+
Difference
73+
<span
74+
aria-hidden="true"
75+
className="inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-400 text-[10px] leading-none text-gray-500"
76+
>
77+
i
78+
</span>
79+
</span>
3780
</th>
3881
</tr>
3982
</thead>
4083
<tbody>
4184
{rows.map((row) => {
42-
const difference = row.expected_amount - row.closing_amount;
85+
// Positive = overage (cashier has more than expected), negative
86+
// = shortage. Must match the sign convention used in the submit
87+
// payload built by POSClosingDialog.handleSubmit.
88+
const difference = row.closing_amount - row.expected_amount;
4389
const hasDifference = Math.abs(difference) > 0.001;
90+
const isTouched = touchedModes.has(row.mode_of_payment);
4491

4592
return (
4693
<tr
4794
key={row.mode_of_payment}
48-
className="border-b border-gray-200 hover:bg-gray-50 transition-colors"
95+
className={cn(
96+
'border-b border-gray-200 hover:bg-gray-50 transition-colors',
97+
!isTouched && 'bg-amber-50/60'
98+
)}
4999
>
50100
<td className="py-3 px-4 text-gray-900 font-medium">
51101
{row.mode_of_payment}
@@ -66,7 +116,7 @@ const ClosingPaymentTable: React.FC<ClosingPaymentTableProps> = ({ rows, onChang
66116
handleClosingAmountChange(row.mode_of_payment, e.target.value)
67117
}
68118
placeholder="0.00"
69-
className="w-full text-center"
119+
className={cn('w-full text-center', !isTouched && 'border-amber-400')}
70120
size="sm"
71121
/>
72122
</td>

0 commit comments

Comments
 (0)