Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cca3077b7a | ||
|
|
6a0d30c8b5 |
9 changed files with 60 additions and 17 deletions
11
Makefile
11
Makefile
|
|
@ -26,7 +26,7 @@ build:
|
|||
|
||||
## deps: install Node.js, pnpm and native-module build tools (Fedora)
|
||||
deps: require-root
|
||||
dnf install -y nodejs gcc-c++ make python3
|
||||
dnf install -y nodejs gcc-c++ make python3 rsync
|
||||
corepack enable
|
||||
|
||||
## install: full production install on a fresh checkout (Fedora)
|
||||
|
|
@ -41,9 +41,14 @@ service: require-root
|
|||
# Dedicated system user (no login, no home).
|
||||
id -u $(SERVICE_USER) >/dev/null 2>&1 || \
|
||||
useradd --system --no-create-home --shell /usr/sbin/nologin $(SERVICE_USER)
|
||||
# Copy the whole tree so pnpm's symlinked node_modules stay intact.
|
||||
# Copy the tree (preserving pnpm's symlinked node_modules) but leave behind
|
||||
# the repo metadata, local dev database, and editor/nix cruft.
|
||||
mkdir -p $(PREFIX)
|
||||
cp -a . $(PREFIX)/
|
||||
rsync -a --delete \
|
||||
--exclude='.git' --exclude='.direnv' --exclude='.env' \
|
||||
--exclude='*.db' --exclude='*.db-wal' --exclude='*.db-shm' \
|
||||
--exclude='$(PREFIX)' \
|
||||
./ $(PREFIX)/
|
||||
# Config + secrets — never clobber an existing env file.
|
||||
mkdir -p $(CONFDIR)
|
||||
test -f $(CONFDIR)/wutzcalc.env || \
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ sudo useradd --system --no-create-home --shell /usr/sbin/nologin wutzcalc
|
|||
|
||||
# 2. Install the built app (run `pnpm install && pnpm build` first)
|
||||
sudo mkdir -p /opt/wutzcalc
|
||||
sudo cp -a . /opt/wutzcalc # or rsync/checkout into place
|
||||
sudo rsync -a --exclude='.git' --exclude='*.db*' ./ /opt/wutzcalc/
|
||||
sudo chown -R root:root /opt/wutzcalc # app dir stays read-only to the service
|
||||
|
||||
# 3. Config + secrets (chmod 600 — holds ADMIN_PASSWORD)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>wutzcalc — Backoffice</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<script>
|
||||
// Set the theme before first paint to avoid a flash of the wrong colors.
|
||||
document.documentElement.setAttribute(
|
||||
'data-theme',
|
||||
localStorage.getItem('wutz.theme') === 'light' ? 'light' : 'dark'
|
||||
);
|
||||
</script>
|
||||
<link rel="stylesheet" href="/src/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>wutzcalc</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<script>
|
||||
// Set the theme before first paint to avoid a flash of the wrong colors.
|
||||
document.documentElement.setAttribute(
|
||||
'data-theme',
|
||||
localStorage.getItem('wutz.theme') === 'light' ? 'light' : 'dark'
|
||||
);
|
||||
</script>
|
||||
<link rel="stylesheet" href="/src/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
14
client/public/favicon.svg
Normal file
14
client/public/favicon.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<!-- handle -->
|
||||
<rect x="19" y="13" width="9" height="11" rx="4.5" fill="none" stroke="#d98500" stroke-width="3"/>
|
||||
<!-- mug body -->
|
||||
<rect x="6" y="11" width="15" height="17" rx="2.5" fill="#f6a821"/>
|
||||
<!-- lighter head of the beer -->
|
||||
<rect x="6" y="11" width="15" height="4" rx="2.5" fill="#ffc34d"/>
|
||||
<!-- foam -->
|
||||
<g fill="#fffaf0">
|
||||
<circle cx="9.5" cy="10" r="4"/>
|
||||
<circle cx="14" cy="8.5" r="4.5"/>
|
||||
<circle cx="18.5" cy="10" r="4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 563 B |
|
|
@ -62,7 +62,7 @@ function Login({ onAuthed }: { onAuthed: () => void }) {
|
|||
value={pw}
|
||||
onInput={(e: any) => setPw(e.currentTarget.value)}
|
||||
/>
|
||||
{err && <div style="color:#ff8a8a">{err}</div>}
|
||||
{err && <div style="color:var(--danger)">{err}</div>}
|
||||
<button type="submit">Anmelden</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -107,7 +107,7 @@ function Stats() {
|
|||
<>
|
||||
<div class="row" style="justify-content: space-between">
|
||||
<h2 style="margin:0">Umsatz nach Tagen</h2>
|
||||
<button onClick={reset} style="background:#5a2a2a; border-color:#7a3a3a">Statistik zurücksetzen</button>
|
||||
<button class="danger" onClick={reset}>Statistik zurücksetzen</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Tag</th><th>Transaktionen</th><th>Bezahlt</th><th>Crew-Transaktionen</th><th>Pfand zurück</th></tr></thead>
|
||||
|
|
@ -227,7 +227,7 @@ function Drinks() {
|
|||
<button onClick={() => patch(d.id, { archived: !d.archived } as any)}>
|
||||
{d.archived ? 'aktivieren' : 'archivieren'}
|
||||
</button>
|
||||
<button onClick={() => del(d)} style="background:#5a2a2a; border-color:#7a3a3a">löschen</button>
|
||||
<button class="danger" onClick={() => del(d)}>löschen</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -377,7 +377,7 @@ function Bars() {
|
|||
<>
|
||||
<h2>Bars</h2>
|
||||
{bars.map(b => (
|
||||
<div key={b.id} style="margin-bottom:16px; padding:8px; border:1px solid #333; border-radius:6px">
|
||||
<div key={b.id} style="margin-bottom:16px; padding:8px; border:1px solid var(--border); border-radius:6px">
|
||||
<div class="row">
|
||||
<label>
|
||||
Name
|
||||
|
|
@ -397,7 +397,7 @@ function Bars() {
|
|||
}
|
||||
/>
|
||||
</label>
|
||||
<button onClick={() => delBar(b)} style="background:#5a2a2a; border-color:#7a3a3a; margin-left:auto">Tresen löschen</button>
|
||||
<button class="danger" onClick={() => delBar(b)} style="margin-left:auto">Tresen löschen</button>
|
||||
</div>
|
||||
<BarDrinkEditor
|
||||
bar={b}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ button {
|
|||
}
|
||||
button:active { background: var(--surface-3); }
|
||||
button:disabled { opacity: 0.4; }
|
||||
button.danger { background: #5a2a2a; border-color: #7a3a3a; color: #fff; }
|
||||
|
||||
/* ---------- Tablet UI (portrait) ---------- */
|
||||
|
||||
|
|
@ -92,6 +93,8 @@ button:disabled { opacity: 0.4; }
|
|||
border-bottom: 1px solid var(--border-soft);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.topbar .brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||
.topbar .logo { flex: 0 0 auto; }
|
||||
.topbar .bar-name { font-size: 26px; font-weight: 800; }
|
||||
.topbar .change { font-size: 17px; padding: 10px 14px; }
|
||||
|
||||
|
|
@ -213,7 +216,7 @@ button:disabled { opacity: 0.4; }
|
|||
cursor: grab;
|
||||
}
|
||||
.dnd-item:active { cursor: grabbing; }
|
||||
.dnd-item.drop-target { border-color: #6aa; background: #1a2630; }
|
||||
.dnd-item.drop-target { border-color: #6aa; background: var(--surface-2); }
|
||||
.dnd-item .grip { opacity: 0.4; cursor: grab; user-select: none; }
|
||||
.dnd-item .dnd-name { flex: 1; }
|
||||
.dnd-list li.muted { padding: 6px 8px; }
|
||||
|
|
|
|||
|
|
@ -78,7 +78,10 @@ export function Sale({ config, onChangeBar }: Props) {
|
|||
return (
|
||||
<div class="tablet">
|
||||
<div class="topbar">
|
||||
<div class="bar-name">{bar.name}</div>
|
||||
<div class="brand">
|
||||
<img class="logo" src="/favicon.svg" alt="" width="28" height="28" />
|
||||
<span class="bar-name">{bar.name}</span>
|
||||
</div>
|
||||
<button class="change" onClick={onChangeBar}>Bar wechseln</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -92,10 +95,12 @@ export function Sale({ config, onChangeBar }: Props) {
|
|||
)}
|
||||
</button>
|
||||
))}
|
||||
<button class="drink pfand-return" onClick={addPfandReturn}>
|
||||
<div class="name">Pfand zurück</div>
|
||||
<div class="price">−{formatCents(bar.pfand_cents)}</div>
|
||||
</button>
|
||||
{bar.pfand_cents > 0 && (
|
||||
<button class="drink pfand-return" onClick={addPfandReturn}>
|
||||
<div class="name">Pfand zurück</div>
|
||||
<div class="price">−{formatCents(bar.pfand_cents)}</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="cart">
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ function localParts(d: Date, tz: string = TZ): LocalParts {
|
|||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
hourCycle: 'h23', // force midnight as 00, never the 24 artifact
|
||||
});
|
||||
const parts: Record<string, string> = {};
|
||||
for (const p of fmt.formatToParts(d)) {
|
||||
|
|
@ -49,7 +49,7 @@ function localParts(d: Date, tz: string = TZ): LocalParts {
|
|||
year: Number(parts.year),
|
||||
month: Number(parts.month),
|
||||
day: Number(parts.day),
|
||||
hour: Number(parts.hour === '24' ? '0' : parts.hour),
|
||||
hour: Number(parts.hour),
|
||||
minute: Number(parts.minute),
|
||||
second: Number(parts.second),
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue