Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a82cb06db | ||
|
|
d89b080da5 | ||
|
|
d80863a169 | ||
|
|
ae79c58d30 | ||
|
|
a0ef6e631e | ||
|
|
d895a5fb7c | ||
|
|
fd765a74c0 | ||
|
|
1f184fab8b | ||
|
|
b6c7c66027 | ||
|
|
33bb87d7f4 | ||
|
|
d4a8a47ccd | ||
|
|
8e8c93410b | ||
|
|
cca59bcace | ||
|
|
432bbcb6f2 | ||
|
|
accdccf897 | ||
|
|
7fc8c6f1e0 | ||
|
|
0eb14a1869 | ||
|
|
c841954c5d | ||
|
|
320c4066f3 | ||
|
|
cda74e65db | ||
|
|
d1a19b043d | ||
|
|
8c4de96432 | ||
|
|
ce7d8a3de5 | ||
|
|
0309f488bd | ||
|
|
d82762430f | ||
|
|
54e5ada03d | ||
|
|
136fe7d355 | ||
|
|
f5b3635773 | ||
|
|
bff8669cd7 | ||
|
|
008462e304 | ||
|
|
9237e911d2 | ||
|
|
5bb0e01136 | ||
|
|
f19827ae91 |
@@ -26,16 +26,15 @@ trigger: always_on
|
|||||||
- **Interaction**: Be proactive, concise, and helpful. Focus on code value.
|
- **Interaction**: Be proactive, concise, and helpful. Focus on code value.
|
||||||
|
|
||||||
## 4. Development Standards
|
## 4. Development Standards
|
||||||
**Tech Stack:**
|
|
||||||
- **Container**: Docker-based application.
|
|
||||||
- **Config**: Configurable port.
|
|
||||||
|
|
||||||
**Coding Style:**
|
**Coding Style:**
|
||||||
- **Typing**: Strict typing where applicable.
|
- **Typing**: Strict typing where applicable.
|
||||||
- **Comments**: Concise, English.
|
- **Comments**: Concise, English.
|
||||||
- **Frontend/UX**:
|
- **Frontend/UX**:
|
||||||
- Priority on Usability.
|
- Priority on Usability.
|
||||||
- **MANDATORY**: Tooltips/Help texts for all interactions.
|
- **MANDATORY**: Tooltips/Help texts for all interactions.
|
||||||
|
- **Versioning**:
|
||||||
|
- version.txt has to be increased for any implemented features or fixed bugs)
|
||||||
|
- a change summary has to be documented in changelog.md
|
||||||
|
|
||||||
## 5. Agentic Workflow & Artifacts
|
## 5. Agentic Workflow & Artifacts
|
||||||
**Core Philosophy**: Plan first, act second.
|
**Core Philosophy**: Plan first, act second.
|
||||||
|
|||||||
@@ -38,8 +38,9 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
|
|||||||
| FR-031 | Authentifizierte Benutzer müssen eine bestehende Bestellung direkt aus der Übersicht stornieren können. | Hoch | v1.0.1 |
|
| FR-031 | Authentifizierte Benutzer müssen eine bestehende Bestellung direkt aus der Übersicht stornieren können. | Hoch | v1.0.1 |
|
||||||
| FR-032 | Nach Bestellschluss (Cutoff-Zeit) dürfen keine neuen Bestellungen oder Stornierungen für diesen Tag möglich sein. | Hoch | v1.0.1 |
|
| FR-032 | Nach Bestellschluss (Cutoff-Zeit) dürfen keine neuen Bestellungen oder Stornierungen für diesen Tag möglich sein. | Hoch | v1.0.1 |
|
||||||
| FR-033 | Es muss möglich sein, dasselbe Menü mehrfach zu bestellen. Bei Mehrfachbestellungen muss die Anzahl angezeigt werden. | Niedrig | v1.0.1 |
|
| FR-033 | Es muss möglich sein, dasselbe Menü mehrfach zu bestellen. Bei Mehrfachbestellungen muss die Anzahl angezeigt werden. | Niedrig | v1.0.1 |
|
||||||
| **Kostentransparenz** | | | |
|
| **Kostentransparenz & Bestellhistorie** | | | |
|
||||||
| FR-040 | Das System muss die Gesamtkosten aller Bestellungen einer Woche automatisch berechnen und anzeigen. | Mittel | v1.1.0 |
|
| FR-040 | Das System muss die Gesamtkosten aller Bestellungen einer Woche automatisch berechnen und anzeigen. | Mittel | v1.1.0 |
|
||||||
|
| FR-041 | Das System muss dem Benutzer eine Bestellhistorie (gruppiert nach Monat und KW) mit Fortschrittsanzeige auf Abruf in einem Modal bereitstellen. Die Historie muss über ein lokales Delta-Caching verfügen, um Ladezeiten zu minimieren. | Mittel | v1.4.0 (Update v1.4.7) |
|
||||||
| **Bestell-Countdown** | | | |
|
| **Bestell-Countdown** | | | |
|
||||||
| FR-050 | Das System muss vor Bestellschluss einen visuell hervorgehobenen Countdown anzeigen. | Mittel | v1.1.0 |
|
| FR-050 | Das System muss vor Bestellschluss einen visuell hervorgehobenen Countdown anzeigen. | Mittel | v1.1.0 |
|
||||||
| **Menü-Flagging & Benachrichtigungen** | | | |
|
| **Menü-Flagging & Benachrichtigungen** | | | |
|
||||||
@@ -53,10 +54,13 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
|
|||||||
| FR-071 | Die Hervorhebung muss anhand von Menüname und Beschreibung erfolgen (Substring-Matching, case-insensitive). | Niedrig | v1.1.0 |
|
| FR-071 | Die Hervorhebung muss anhand von Menüname und Beschreibung erfolgen (Substring-Matching, case-insensitive). | Niedrig | v1.1.0 |
|
||||||
| FR-072 | Hervorgehobene Menüs müssen ein Tag-Badge mit dem matchenden Schlagwort anzeigen. | Niedrig | v1.2.4 |
|
| FR-072 | Hervorgehobene Menüs müssen ein Tag-Badge mit dem matchenden Schlagwort anzeigen. | Niedrig | v1.2.4 |
|
||||||
| FR-073 | Die Nächste-Woche-Navigation muss die Anzahl der dort gefundenen Highlights als Badge anzeigen. | Niedrig | v1.2.5 |
|
| FR-073 | Die Nächste-Woche-Navigation muss die Anzahl der dort gefundenen Highlights als Badge anzeigen. | Niedrig | v1.2.5 |
|
||||||
| **Darstellung & Theming** | | | |
|
|
||||||
| FR-080 | Das System muss einen hellen und einen dunklen Darstellungsmodus (Light/Dark Theme) unterstützen. | Niedrig | v1.0.1 |
|
| FR-080 | Das System muss einen hellen und einen dunklen Darstellungsmodus (Light/Dark Theme) unterstützen. | Niedrig | v1.0.1 |
|
||||||
| FR-081 | Die Theme-Präferenz des Benutzers muss persistent gespeichert werden. | Niedrig | v1.0.1 |
|
| FR-081 | Die Theme-Präferenz des Benutzers muss persistent gespeichert werden. | Niedrig | v1.0.1 |
|
||||||
| FR-082 | Das System muss beim erstmaligen Laden die Betriebssystem-Präferenz für das Farbschema berücksichtigen. | Niedrig | v1.0.1 |
|
| FR-082 | Das System muss beim erstmaligen Laden die Betriebssystem-Präferenz für das Farbschema berücksichtigen. | Niedrig | v1.0.1 |
|
||||||
|
| **Header UI & Navigation** | | | |
|
||||||
|
| FR-090 | Die Hauptnavigation (Wochen-Toggles) muss linksbündig neben dem App-Titel positioniert sein. | Niedrig | v1.5.0 |
|
||||||
|
| FR-091 | Ein dynamisches Alarm-Icon im Header muss den Überwachungsstatus geflaggter Menüs anzeigen (Gelb=Überwachung aktiv aber kein Menü verfügbar, Grün=Mindestens ein Menü verfügbar, Versteckt=keine Flags). Der Tooltip muss den Zeitpunkt der letzten Prüfung als relativen String (z.B. "vor 4 Min.") enthalten. | Mittel | v1.5.0 (Update v1.4.10) |
|
||||||
|
| FR-092 | Sobald über den Daten-Refresh erstmals Menüdaten für die Nächste Woche geladen werden, muss der entsprechende Navigation-Button animiert und farblich (Gelb) hervorgehoben werden. Zusätzlich muss einmalig ein Hinweis eingeblendet werden. Bei Klick auf den Button muss die Hervorhebung erlöschen. | Mittel | v1.6.0 |
|
||||||
| **Benutzer-Feedback** | | | |
|
| **Benutzer-Feedback** | | | |
|
||||||
| FR-090 | Alle benutzerrelevanten Aktionen (Bestellung, Stornierung, Fehler) müssen durch nicht-blockierende Benachrichtigungen (Toasts) bestätigt werden. | Mittel | v1.0.1 |
|
| FR-090 | Alle benutzerrelevanten Aktionen (Bestellung, Stornierung, Fehler) müssen durch nicht-blockierende Benachrichtigungen (Toasts) bestätigt werden. | Mittel | v1.0.1 |
|
||||||
| FR-091 | Bei einem Verbindungsfehler muss ein Fehlerdialog mit Fallback-Link zur Originalseite angezeigt werden. | Mittel | v1.0.1 |
|
| FR-091 | Bei einem Verbindungsfehler muss ein Fehlerdialog mit Fallback-Link zur Originalseite angezeigt werden. | Mittel | v1.0.1 |
|
||||||
@@ -68,6 +72,7 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
|
|||||||
| FR-112 | Benutzer müssen eine Versionsliste mit Installationslinks einsehen können (Versionsmenü). | Niedrig | v1.3.0 |
|
| FR-112 | Benutzer müssen eine Versionsliste mit Installationslinks einsehen können (Versionsmenü). | Niedrig | v1.3.0 |
|
||||||
| FR-113 | Es muss möglich sein, zu einer älteren Version zurückzukehren (Downgrade). | Niedrig | v1.3.0 |
|
| FR-113 | Es muss möglich sein, zu einer älteren Version zurückzukehren (Downgrade). | Niedrig | v1.3.0 |
|
||||||
| FR-114 | Ein Dev-Mode muss es ermöglichen, zwischen stabilen Releases und Entwicklungs-Tags umzuschalten. | Niedrig | v1.3.0 |
|
| FR-114 | Ein Dev-Mode muss es ermöglichen, zwischen stabilen Releases und Entwicklungs-Tags umzuschalten. | Niedrig | v1.3.0 |
|
||||||
|
| FR-115 | Das Versionsmenü muss Links zur Erstellung von Feature-Requests und Bug-Reports auf GitHub enthalten. | Niedrig | v1.4.4 |
|
||||||
|
|
||||||
## 3. Nicht-funktionale Anforderungen
|
## 3. Nicht-funktionale Anforderungen
|
||||||
|
|
||||||
@@ -88,4 +93,4 @@ Das System umfasst die Darstellung von Menüplänen in einer Wochenübersicht, d
|
|||||||
* **Datenhaltung**: Clientseitig via `localStorage` (Menü-Cache, Flags, Highlights, Theme) und `sessionStorage` (Auth-Token).
|
* **Datenhaltung**: Clientseitig via `localStorage` (Menü-Cache, Flags, Highlights, Theme) und `sessionStorage` (Auth-Token).
|
||||||
* **Build**: Bash-basiertes Build-Script, das Bookmarklet-URL, Standalone-HTML und Installer-Seite generiert.
|
* **Build**: Bash-basiertes Build-Script, das Bookmarklet-URL, Standalone-HTML und Installer-Seite generiert.
|
||||||
* **Versionierung**: SemVer, verwaltet über GitHub Releases/Tags.
|
* **Versionierung**: SemVer, verwaltet über GitHub Releases/Tags.
|
||||||
* **Tests**: Python-basierte Build-Tests + Node.js-basierte Logik-Tests.
|
* **Tests**: Python-basierte Build-Tests (`python3`) + Node.js-basierte Logik-Tests.
|
||||||
|
|||||||
BIN
__pycache__/test_build.cpython-312-pytest-9.0.2.pyc
Executable file
BIN
__pycache__/test_build.cpython-312-pytest-9.0.2.pyc
Executable file
Binary file not shown.
1
bessa_orders_debug.json
Executable file
1
bessa_orders_debug.json
Executable file
@@ -0,0 +1 @@
|
|||||||
|
{"next":null,"previous":null,"results":[]}
|
||||||
@@ -171,7 +171,7 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 40px; color: #5c6b7f; font-size: 0.8rem;">
|
<div style="text-align: center; margin-top: 40px; color: #5c6b7f; font-size: 0.8rem;">
|
||||||
<p>Powered by <strong>Kaufi-Kitchen</strong> 👨🍳</p>
|
<p>Powered by <strong>Kaufis-Kitchen</strong> 👨🍳</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -267,23 +267,4 @@ if [ $TEST_EXIT -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
echo "✅ All build tests passed."
|
echo "✅ All build tests passed."
|
||||||
|
|
||||||
# === 5. Commit, tag, and push ===
|
|
||||||
echo ""
|
|
||||||
echo "=== Committing & Pushing ==="
|
|
||||||
git add -A
|
|
||||||
git commit -m "dist files for $VERSION built" --allow-empty
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Tagging $VERSION ==="
|
|
||||||
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
|
||||||
git tag -f "$VERSION"
|
|
||||||
echo "🔄 Tag $VERSION moved to current commit."
|
|
||||||
else
|
|
||||||
git tag "$VERSION"
|
|
||||||
echo "✅ Created tag: $VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
git push
|
|
||||||
git push origin --force tag "$VERSION"
|
|
||||||
git push github --force tag "$VERSION"
|
|
||||||
echo "✅ Pushed commit + tag $VERSION"
|
|
||||||
|
|||||||
55
changelog.md
55
changelog.md
@@ -1,3 +1,58 @@
|
|||||||
|
## v1.4.16
|
||||||
|
- ⚡ **Feature**: Ein Button "Lokalen Cache leeren" wurde zum Versionen-Menü hinzugefügt, um bei hartnäckigen lokalen Fehlern alle Caches und Sessions bereinigen zu können, ohne die Entwicklertools (F12) des Browsers bemühen zu müssen.
|
||||||
|
|
||||||
|
## v1.4.15
|
||||||
|
- 🧹 **Bugfix**: In der Vergangenheit gesetzte Alarme/Flags wurden nicht zuverlässig gelöscht. Dies ist nun behoben, sodass verfallene Menüs nach 10:00 Uhr bzw. an vergangenen Tagen automatisch aus dem Tracker verschwinden.
|
||||||
|
|
||||||
|
## v1.4.14
|
||||||
|
- 🐛 **Bugfix**: Alarmglocke versteckt sich jetzt zuverlässig auch auf Endgeräten mit CSS Konflikten
|
||||||
|
- 🚀 **Feature**: Sofortige API-Aktualisierung (Refresh) bei Aktivierung eines Menüalarms
|
||||||
|
- ⚡ **Optimierung**: "Unbekannt" im letzten Refresh-Zeitpunkt wird abgefangen und zeigt initial "gerade eben"
|
||||||
|
|
||||||
|
## v1.4.13 (2026-02-24)
|
||||||
|
- **Fix**: Die Farben der Glocke funktionieren nun verlässlich, da CSS-Variablen durch direkte Hex-Codes ersetzt wurden.
|
||||||
|
|
||||||
|
## v1.4.12 (2026-02-24)
|
||||||
|
- **Fix**: Das Glocken-Icon sollte nun endgültig versteckt bleiben, wenn keine Benachrichtigungen aktiv sind (CSS-Kollision mit `.hidden` behoben).
|
||||||
|
|
||||||
|
## v1.4.11 (2026-02-24)
|
||||||
|
- **Feature**: Das Versionsmenü prüft nun im Hintergrund direkt beim Öffnen nach neuen Versionen und aktualisiert die Liste automatisch, selbst wenn eine veraltete Liste noch im Cache liegt.
|
||||||
|
|
||||||
|
## v1.4.10 (2026-02-24)
|
||||||
|
- **Fix**: Die Farben der Benachrichtigungs-Glocke wurden korrigiert: Sie ist nun gelb, während man auf ein Menü wartet, und wird grün, sobald eines verfügbar ist.
|
||||||
|
|
||||||
|
## v1.4.9 (2026-02-24)
|
||||||
|
- **Fix**: Das Glocken-Icon für Benachrichtigungen wird nun direkt beim Start (wenn Daten aus dem lokalen Cache geladen werden) korrekt angezeigt.
|
||||||
|
|
||||||
|
## v1.4.8 (2026-02-24)
|
||||||
|
- **Fix**: Die Benachrichtigungs-Glocke wird nun korrekt in Gelb dargestellt, wenn beobachtete Menüs verfügbar sind.
|
||||||
|
- **Tools**: Fehler in Testskript behoben, der den CI/CD Build verlangsamt hat.
|
||||||
|
|
||||||
|
## v1.4.7 (2026-02-24)
|
||||||
|
- **Performance**: Die Bestellhistorie nutzt nun einen inkrementellen Delta-Cache anstatt immer alle Seiten von der API herunterzuladen, was die Ladezeiten für Vielbesteller enorm reduziert.
|
||||||
|
|
||||||
|
## v1.4.6 (2026-02-24)
|
||||||
|
- **Fix**: Die Umrandung für bereits bestellte Menüs der vergangenen Tage ist nun ebenfalls einheitlich violett statt blau.
|
||||||
|
|
||||||
|
## v1.4.5 (2026-02-24)
|
||||||
|
- **Fix**: Doppelten Scrollbalken in der Versionen-Liste entfernt.
|
||||||
|
|
||||||
|
## v1.4.4 (2026-02-24)
|
||||||
|
- **Feature**: Das Versionsmenü enthält nun direkte Links zu GitHub, um Fehler zu melden oder neue Features vorzuschlagen.
|
||||||
|
|
||||||
|
## v1.4.3 (2026-02-24)
|
||||||
|
- **Fix**: Der Rahmen des "Heute Bestellt" Menüs ist nun konsequent violett (passend zum Glow-Effekt).
|
||||||
|
|
||||||
|
## v1.4.2 (2026-02-23)
|
||||||
|
- **Fix**: Das "Heute Bestellt" Menü leuchtet nun stimmig im Design-Violett statt Blau.
|
||||||
|
- **Fix**: Abfangen des GitHub API Rate Limit (403) im Versionsdialog mit einer freundlicheren Fehlermeldung, da der User-Agent im Browser nicht manuell gesetzt werden darf.
|
||||||
|
|
||||||
|
## v1.4.1 (2026-02-22)
|
||||||
|
- **UX Verbesserungen**: Bestellhistorie gruppiert nach Jahren und Monaten mittels einklappbarem Akkordeon. Monatssummen integriert und Stati farblich abgehoben (Offen, Abgeschlossen, Storniert).
|
||||||
|
|
||||||
|
## v1.4.0 (2026-02-22)
|
||||||
|
- **Feature**: Bestellhistorie per Knopfdruck abrufbar. Übersichtliche Darstellung, gruppiert nach Monaten und Kalenderwochen, inklusive Stornos. 📜✨
|
||||||
|
|
||||||
## v1.3.2 (2026-02-19)
|
## v1.3.2 (2026-02-19)
|
||||||
- **Fix**: Falsche Anzahl an Highlight-Menüs im "Nächste Woche"-Badge korrigiert (zählte alle Menüs statt nur Highlights). 🐛
|
- **Fix**: Falsche Anzahl an Highlight-Menüs im "Nächste Woche"-Badge korrigiert (zählte alle Menüs statt nur Highlights). 🐛
|
||||||
|
|
||||||
|
|||||||
10
cors_server.py
Executable file
10
cors_server.py
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
|
||||||
|
class CORSRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def end_headers(self):
|
||||||
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
super().end_headers()
|
||||||
|
|
||||||
|
with socketserver.TCPServer(("127.0.0.1", 8080), CORSRequestHandler) as httpd:
|
||||||
|
httpd.serve_forever()
|
||||||
4
dist/bookmarklet-payload.js
vendored
4
dist/bookmarklet-payload.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bookmarklet.txt
vendored
2
dist/bookmarklet.txt
vendored
File diff suppressed because one or more lines are too long
86
dist/install.html
vendored
86
dist/install.html
vendored
File diff suppressed because one or more lines are too long
807
dist/kantine-standalone.html
vendored
807
dist/kantine-standalone.html
vendored
File diff suppressed because it is too large
Load Diff
565
kantine.js
565
kantine.js
@@ -74,6 +74,13 @@
|
|||||||
<h1>Kantinen Übersicht <small class="version-tag" style="font-size: 0.6em; opacity: 0.7; font-weight: 400; cursor: pointer;" title="Klick für Versionsmenü">{{VERSION}}</small></h1>
|
<h1>Kantinen Übersicht <small class="version-tag" style="font-size: 0.6em; opacity: 0.7; font-weight: 400; cursor: pointer;" title="Klick für Versionsmenü">{{VERSION}}</small></h1>
|
||||||
<div id="last-updated-subtitle" class="subtitle"></div>
|
<div id="last-updated-subtitle" class="subtitle"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-group" style="margin-left: 1rem;">
|
||||||
|
<button id="btn-this-week" class="nav-btn active">Diese Woche</button>
|
||||||
|
<button id="btn-next-week" class="nav-btn">Nächste Woche</button>
|
||||||
|
</div>
|
||||||
|
<button id="alarm-bell" class="icon-btn hidden" aria-label="Benachrichtigungen" title="Keine beobachteten Menüs" style="margin-left: -0.5rem;">
|
||||||
|
<span class="material-icons-round" id="alarm-bell-icon" style="color:var(--text-secondary); transition: color 0.3s;">notifications</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-center-wrapper">
|
<div class="header-center-wrapper">
|
||||||
<div id="header-week-info" class="header-week-info"></div>
|
<div id="header-week-info" class="header-week-info"></div>
|
||||||
@@ -83,13 +90,12 @@
|
|||||||
<button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren" title="Menüdaten neu laden">
|
<button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren" title="Menüdaten neu laden">
|
||||||
<span class="material-icons-round">refresh</span>
|
<span class="material-icons-round">refresh</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-history" class="icon-btn" aria-label="Bestellhistorie" title="Bestellhistorie">
|
||||||
|
<span class="material-icons-round">receipt_long</span>
|
||||||
|
</button>
|
||||||
<button id="btn-highlights" class="icon-btn" aria-label="Persönliche Highlights verwalten" title="Persönliche Highlights verwalten">
|
<button id="btn-highlights" class="icon-btn" aria-label="Persönliche Highlights verwalten" title="Persönliche Highlights verwalten">
|
||||||
<span class="material-icons-round">label</span>
|
<span class="material-icons-round">label</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="nav-group">
|
|
||||||
<button id="btn-this-week" class="nav-btn active">Diese Woche</button>
|
|
||||||
<button id="btn-next-week" class="nav-btn">Nächste Woche</button>
|
|
||||||
</div>
|
|
||||||
<button id="theme-toggle" class="icon-btn" aria-label="Toggle Theme">
|
<button id="theme-toggle" class="icon-btn" aria-label="Toggle Theme">
|
||||||
<span class="material-icons-round theme-icon">light_mode</span>
|
<span class="material-icons-round theme-icon">light_mode</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -173,6 +179,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="history-modal" class="modal hidden">
|
||||||
|
<div class="modal-content history-modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Bestellhistorie</h2>
|
||||||
|
<button id="btn-history-close" class="icon-btn" aria-label="Close">
|
||||||
|
<span class="material-icons-round">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="history-loading" class="hidden">
|
||||||
|
<p id="history-progress-text" style="text-align: center; margin-bottom: 1rem; color: var(--text-secondary);">Lade Historie...</p>
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div id="history-progress-fill" class="progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="history-content">
|
||||||
|
<!-- Dynamically populated -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="version-modal" class="modal hidden">
|
<div id="version-modal" class="modal hidden">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -191,9 +221,20 @@
|
|||||||
<span>Dev-Mode (alle Tags anzeigen)</span>
|
<span>Dev-Mode (alle Tags anzeigen)</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="version-list-container" style="margin-top:1rem;">
|
<div id="version-list-container" style="margin-top:1rem; max-height: 250px; overflow-y: auto;">
|
||||||
<p style="color:var(--text-secondary);">Lade Versionen...</p>
|
<p style="color:var(--text-secondary);">Lade Versionen...</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border-color); display: flex; flex-direction: column; gap: 0.75rem; font-size: 0.9em;">
|
||||||
|
<a href="https://github.com/TauNeutrino/kantine-overview/issues" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Melde einen Fehler auf GitHub">
|
||||||
|
<span class="material-icons-round" style="font-size: 1.2em;">bug_report</span> Fehler melden
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/TauNeutrino/kantine-overview/discussions/categories/ideas" target="_blank" rel="noopener noreferrer" style="color: var(--primary-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem;" title="Schlage ein neues Feature auf GitHub vor">
|
||||||
|
<span class="material-icons-round" style="font-size: 1.2em;">lightbulb</span> Feature vorschlagen
|
||||||
|
</a>
|
||||||
|
<button id="btn-clear-cache" style="background: none; border: none; padding: 0; color: var(--error-color); text-decoration: none; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; text-align: left; font-size: inherit; font-family: inherit;" title="Löscht alle lokalen Daten & erzwingt einen Neuladen">
|
||||||
|
<span class="material-icons-round" style="font-size: 1.2em;">delete_forever</span> Lokalen Cache leeren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,17 +276,26 @@
|
|||||||
const btnAddTag = document.getElementById('btn-add-tag');
|
const btnAddTag = document.getElementById('btn-add-tag');
|
||||||
const tagInput = document.getElementById('tag-input');
|
const tagInput = document.getElementById('tag-input');
|
||||||
|
|
||||||
btnHighlights.addEventListener('click', () => {
|
// History Modal
|
||||||
highlightsModal.classList.remove('hidden');
|
const btnHistory = document.getElementById('btn-history');
|
||||||
renderTagsList();
|
const historyModal = document.getElementById('history-modal');
|
||||||
tagInput.focus();
|
const btnHistoryClose = document.getElementById('btn-history-close');
|
||||||
|
|
||||||
|
btnHistory.addEventListener('click', () => {
|
||||||
|
if (!authToken) {
|
||||||
|
loginModal.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
historyModal.classList.remove('hidden');
|
||||||
|
fetchFullOrderHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
btnHighlightsClose.addEventListener('click', () => {
|
btnHistoryClose.addEventListener('click', () => {
|
||||||
highlightsModal.classList.add('hidden');
|
historyModal.classList.add('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('click', (e) => {
|
window.addEventListener('click', (e) => {
|
||||||
|
if (e.target === historyModal) historyModal.classList.add('hidden');
|
||||||
if (e.target === highlightsModal) highlightsModal.classList.add('hidden');
|
if (e.target === highlightsModal) highlightsModal.classList.add('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -268,6 +318,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const btnClearCache = document.getElementById('btn-clear-cache');
|
||||||
|
if (btnClearCache) {
|
||||||
|
btnClearCache.addEventListener('click', () => {
|
||||||
|
if (confirm('Möchtest du wirklich alle lokalen Daten (inkl. Login-Session, Cache und Einstellungen) löschen? Die Seite wird danach neu geladen.')) {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('click', (e) => {
|
window.addEventListener('click', (e) => {
|
||||||
if (e.target === versionModal) versionModal.classList.add('hidden');
|
if (e.target === versionModal) versionModal.classList.add('hidden');
|
||||||
});
|
});
|
||||||
@@ -318,6 +379,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
btnNextWeek.addEventListener('click', () => {
|
btnNextWeek.addEventListener('click', () => {
|
||||||
|
btnNextWeek.classList.remove('new-week-available');
|
||||||
if (displayMode !== 'next-week') {
|
if (displayMode !== 'next-week') {
|
||||||
displayMode = 'next-week';
|
displayMode = 'next-week';
|
||||||
btnNextWeek.classList.add('active');
|
btnNextWeek.classList.add('active');
|
||||||
@@ -513,6 +575,276 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === History Modal Flow ===
|
||||||
|
let fullOrderHistoryCache = null;
|
||||||
|
|
||||||
|
async function fetchFullOrderHistory() {
|
||||||
|
const historyLoading = document.getElementById('history-loading');
|
||||||
|
const historyContent = document.getElementById('history-content');
|
||||||
|
const progressFill = document.getElementById('history-progress-fill');
|
||||||
|
const progressText = document.getElementById('history-progress-text');
|
||||||
|
|
||||||
|
// Check local storage cache (we still use memory cache if available)
|
||||||
|
let localCache = [];
|
||||||
|
if (fullOrderHistoryCache) {
|
||||||
|
localCache = fullOrderHistoryCache;
|
||||||
|
} else {
|
||||||
|
const ls = localStorage.getItem('kantine_history_cache');
|
||||||
|
if (ls) {
|
||||||
|
try {
|
||||||
|
localCache = JSON.parse(ls);
|
||||||
|
fullOrderHistoryCache = localCache;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('History cache parse error', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show cached version immediately if we have one
|
||||||
|
if (localCache.length > 0) {
|
||||||
|
renderHistory(localCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authToken) return;
|
||||||
|
|
||||||
|
// Start background delta sync
|
||||||
|
if (localCache.length === 0) {
|
||||||
|
historyContent.innerHTML = '';
|
||||||
|
historyLoading.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
progressFill.style.width = '0%';
|
||||||
|
progressText.textContent = localCache.length > 0 ? 'Suche nach neuen Bestellungen...' : 'Lade Bestellhistorie...';
|
||||||
|
if (localCache.length > 0) historyLoading.classList.remove('hidden');
|
||||||
|
|
||||||
|
let nextUrl = localCache.length > 0
|
||||||
|
? `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=5`
|
||||||
|
: `${API_BASE}/user/orders/?venue=${VENUE_ID}&ordering=-created&limit=50`;
|
||||||
|
let fetchedOrders = [];
|
||||||
|
let totalCount = 0;
|
||||||
|
let requiresFullFetch = localCache.length === 0;
|
||||||
|
let deltaComplete = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (nextUrl && !deltaComplete) {
|
||||||
|
const response = await fetch(nextUrl, { headers: apiHeaders(authToken) });
|
||||||
|
if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.count && totalCount === 0) {
|
||||||
|
totalCount = data.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = data.results || [];
|
||||||
|
|
||||||
|
for (const order of results) {
|
||||||
|
// Check if we hit an order that is already in our cache AND has the exact same state/update time
|
||||||
|
// Bessa returns 'updated' timestamp, we can use it to determine if anything changed
|
||||||
|
const existingOrderIndex = localCache.findIndex(cached => cached.id === order.id);
|
||||||
|
|
||||||
|
if (!requiresFullFetch && existingOrderIndex !== -1) {
|
||||||
|
const existingOrder = localCache[existingOrderIndex];
|
||||||
|
// If order exists and wasn't updated since our cache, we've reached the point
|
||||||
|
// where everything older is already correctly cached.
|
||||||
|
// order.updated is an ISO string like "2025-02-18T10:30:15.123456Z"
|
||||||
|
if (existingOrder.updated === order.updated && existingOrder.order_state === order.order_state) {
|
||||||
|
deltaComplete = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchedOrders.push(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress
|
||||||
|
if (!deltaComplete && requiresFullFetch) {
|
||||||
|
if (totalCount > 0) {
|
||||||
|
const pct = Math.round((fetchedOrders.length / totalCount) * 100);
|
||||||
|
progressFill.style.width = `${pct}%`;
|
||||||
|
progressText.textContent = `Lade Bestellung ${fetchedOrders.length} von ${totalCount}...`;
|
||||||
|
} else {
|
||||||
|
progressText.textContent = `Lade Bestellung ${fetchedOrders.length}...`;
|
||||||
|
}
|
||||||
|
} else if (!deltaComplete) {
|
||||||
|
progressText.textContent = `${fetchedOrders.length} neue/geänderte Bestellungen gefunden...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextUrl = deltaComplete ? null : data.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge fetched orders with cache
|
||||||
|
if (fetchedOrders.length > 0) {
|
||||||
|
// We have new/updated orders. We need to merge them into the cache.
|
||||||
|
// 1. Create a map of the existing cache for quick ID lookup
|
||||||
|
const cacheMap = new Map(localCache.map(o => [o.id, o]));
|
||||||
|
|
||||||
|
// 2. Update/Insert the newly fetched orders
|
||||||
|
for (const order of fetchedOrders) {
|
||||||
|
cacheMap.set(order.id, order); // Overwrites existing, or adds new
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Convert back to array and sort by created date (descending)
|
||||||
|
const mergedOrders = Array.from(cacheMap.values());
|
||||||
|
mergedOrders.sort((a, b) => new Date(b.created) - new Date(a.created));
|
||||||
|
|
||||||
|
fullOrderHistoryCache = mergedOrders;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('kantine_history_cache', JSON.stringify(mergedOrders));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('History cache write error', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the updated history
|
||||||
|
renderHistory(fullOrderHistoryCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in history sync:', error);
|
||||||
|
if (localCache.length === 0) {
|
||||||
|
historyContent.innerHTML = `<p style="color:var(--error-color);text-align:center;">Fehler beim Laden der Historie.</p>`;
|
||||||
|
} else {
|
||||||
|
showToast('Hintergrund-Synchronisation fehlgeschlagen', 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
historyLoading.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHistory(orders) {
|
||||||
|
const content = document.getElementById('history-content');
|
||||||
|
if (!orders || orders.length === 0) {
|
||||||
|
content.innerHTML = '<p style="text-align:center;color:var(--text-secondary);padding:20px;">Keine Bestellungen gefunden.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by Year -> Month -> Week Number (KW)
|
||||||
|
const groups = {};
|
||||||
|
|
||||||
|
orders.forEach(order => {
|
||||||
|
const d = new Date(order.date);
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = d.getMonth();
|
||||||
|
const monthKey = `${y}-${m.toString().padStart(2, '0')}`;
|
||||||
|
const monthName = d.toLocaleString('de-AT', { month: 'long' }); // Only month name
|
||||||
|
|
||||||
|
const kw = getISOWeek(d);
|
||||||
|
|
||||||
|
if (!groups[y]) {
|
||||||
|
groups[y] = { year: y, months: {} };
|
||||||
|
}
|
||||||
|
if (!groups[y].months[monthKey]) {
|
||||||
|
groups[y].months[monthKey] = { name: monthName, year: y, monthIndex: m, count: 0, total: 0, weeks: {} };
|
||||||
|
}
|
||||||
|
if (!groups[y].months[monthKey].weeks[kw]) {
|
||||||
|
groups[y].months[monthKey].weeks[kw] = { label: `KW ${kw}`, items: [], count: 0, total: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = order.items || [];
|
||||||
|
items.forEach(item => {
|
||||||
|
const itemPrice = parseFloat(item.price || order.total || 0);
|
||||||
|
groups[y].months[monthKey].weeks[kw].items.push({
|
||||||
|
date: order.date,
|
||||||
|
name: item.name || 'Menü',
|
||||||
|
price: itemPrice,
|
||||||
|
state: order.order_state // 9 is cancelled, 5 is active, 8 is completed
|
||||||
|
});
|
||||||
|
|
||||||
|
if (order.order_state !== 9) {
|
||||||
|
groups[y].months[monthKey].weeks[kw].count++;
|
||||||
|
groups[y].months[monthKey].weeks[kw].total += itemPrice;
|
||||||
|
groups[y].months[monthKey].count++;
|
||||||
|
groups[y].months[monthKey].total += itemPrice;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate HTML
|
||||||
|
const sortedYears = Object.keys(groups).sort((a, b) => b - a);
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
sortedYears.forEach(yKey => {
|
||||||
|
const yearGroup = groups[yKey];
|
||||||
|
html += `<div class="history-year-group">
|
||||||
|
<h2 class="history-year-header">${yearGroup.year}</h2>`;
|
||||||
|
|
||||||
|
const sortedMonths = Object.keys(yearGroup.months).sort((a, b) => b.localeCompare(a));
|
||||||
|
|
||||||
|
sortedMonths.forEach(mKey => {
|
||||||
|
const monthGroup = yearGroup.months[mKey];
|
||||||
|
|
||||||
|
html += `<div class="history-month-group">
|
||||||
|
<div class="history-month-header" tabindex="0" role="button" aria-expanded="false">
|
||||||
|
<div style="display:flex; flex-direction:column; gap:4px;">
|
||||||
|
<span>${monthGroup.name}</span>
|
||||||
|
<div class="history-month-summary">
|
||||||
|
<span>${monthGroup.count} Bestellungen • <strong>€${monthGroup.total.toFixed(2)}</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="material-icons-round">expand_more</span>
|
||||||
|
</div>
|
||||||
|
<div class="history-month-content">`;
|
||||||
|
|
||||||
|
const sortedKWs = Object.keys(monthGroup.weeks).sort((a, b) => parseInt(b) - parseInt(a));
|
||||||
|
|
||||||
|
sortedKWs.forEach(kw => {
|
||||||
|
const week = monthGroup.weeks[kw];
|
||||||
|
html += `<div class="history-week-group">
|
||||||
|
<div class="history-week-header">
|
||||||
|
<strong>${week.label}</strong>
|
||||||
|
<span>${week.count} Bestellungen • <strong>€${week.total.toFixed(2)}</strong></span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
week.items.forEach(item => {
|
||||||
|
const dateObj = new Date(item.date);
|
||||||
|
const dayStr = dateObj.toLocaleDateString('de-AT', { weekday: 'short', day: '2-digit', month: '2-digit' });
|
||||||
|
|
||||||
|
let statusBadge = '';
|
||||||
|
if (item.state === 9) {
|
||||||
|
statusBadge = '<span class="history-item-status">Storniert</span>';
|
||||||
|
} else if (item.state === 8) {
|
||||||
|
statusBadge = '<span class="history-item-status">Abgeschlossen</span>';
|
||||||
|
} else {
|
||||||
|
statusBadge = '<span class="history-item-status">Übertragen</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="history-item ${item.state === 9 ? 'history-item-cancelled' : ''}">
|
||||||
|
<div style="font-size: 0.85rem; color: var(--text-secondary);">${dayStr}</div>
|
||||||
|
<div class="history-item-details">
|
||||||
|
<span class="history-item-name">${escapeHtml(item.name)}</span>
|
||||||
|
<div>${statusBadge}</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-item-price ${item.state === 9 ? 'history-item-price-cancelled' : ''}">€${item.price.toFixed(2)}</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
html += `</div>`;
|
||||||
|
});
|
||||||
|
html += `</div></div>`; // Close month-content and month-group
|
||||||
|
});
|
||||||
|
html += `</div>`; // Close year-group
|
||||||
|
});
|
||||||
|
|
||||||
|
content.innerHTML = html;
|
||||||
|
|
||||||
|
// Bind Accordion Click Events via JS
|
||||||
|
const monthHeaders = content.querySelectorAll('.history-month-header');
|
||||||
|
monthHeaders.forEach(header => {
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const parentGroup = header.parentElement;
|
||||||
|
const isOpen = parentGroup.classList.contains('open');
|
||||||
|
|
||||||
|
// Toggle current
|
||||||
|
if (isOpen) {
|
||||||
|
parentGroup.classList.remove('open');
|
||||||
|
header.setAttribute('aria-expanded', 'false');
|
||||||
|
} else {
|
||||||
|
parentGroup.classList.add('open');
|
||||||
|
header.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// === Place Order ===
|
// === Place Order ===
|
||||||
async function placeOrder(date, articleId, name, price, description) {
|
async function placeOrder(date, articleId, name, price, description) {
|
||||||
if (!authToken) return;
|
if (!authToken) return;
|
||||||
@@ -574,6 +906,7 @@
|
|||||||
|
|
||||||
if (response.ok || response.status === 201) {
|
if (response.ok || response.status === 201) {
|
||||||
showToast(`Bestellt: ${name}`, 'success');
|
showToast(`Bestellt: ${name}`, 'success');
|
||||||
|
fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
|
||||||
await fetchOrders();
|
await fetchOrders();
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -603,6 +936,7 @@
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showToast(`Storniert: ${name}`, 'success');
|
showToast(`Storniert: ${name}`, 'success');
|
||||||
|
fullOrderHistoryCache = null; // Clear memory cache so next history open triggers delta sync
|
||||||
await fetchOrders();
|
await fetchOrders();
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -619,31 +953,172 @@
|
|||||||
localStorage.setItem('kantine_flags', JSON.stringify([...userFlags]));
|
localStorage.setItem('kantine_flags', JSON.stringify([...userFlags]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshFlaggedItems() {
|
||||||
|
if (userFlags.size === 0) return;
|
||||||
|
const token = authToken || GUEST_TOKEN;
|
||||||
|
const datesToFetch = new Set();
|
||||||
|
|
||||||
|
for (const flagId of userFlags) {
|
||||||
|
const [dateStr] = flagId.split('_');
|
||||||
|
datesToFetch.add(dateStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = false;
|
||||||
|
for (const dateStr of datesToFetch) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${API_BASE}/venues/${VENUE_ID}/menu/${MENU_ID}/${dateStr}/`, {
|
||||||
|
headers: apiHeaders(token)
|
||||||
|
});
|
||||||
|
if (!resp.ok) continue;
|
||||||
|
const data = await resp.json();
|
||||||
|
const menuGroups = data.results || [];
|
||||||
|
let dayItems = [];
|
||||||
|
for (const group of menuGroups) {
|
||||||
|
if (group.items && Array.isArray(group.items)) {
|
||||||
|
dayItems = dayItems.concat(group.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update allWeeks in memory
|
||||||
|
for (let week of allWeeks) {
|
||||||
|
if (!week.days) continue;
|
||||||
|
let dayObj = week.days.find(d => d.date === dateStr);
|
||||||
|
if (dayObj) {
|
||||||
|
dayObj.items = dayItems.map(item => {
|
||||||
|
const isUnlimited = item.amount_tracking === false;
|
||||||
|
const hasStock = parseInt(item.available_amount) > 0;
|
||||||
|
return {
|
||||||
|
id: `${dateStr}_${item.id}`,
|
||||||
|
articleId: item.id,
|
||||||
|
name: item.name || 'Unknown',
|
||||||
|
description: item.description || '',
|
||||||
|
price: parseFloat(item.price) || 0,
|
||||||
|
available: isUnlimited || hasStock,
|
||||||
|
availableAmount: parseInt(item.available_amount) || 0,
|
||||||
|
amountTracking: item.amount_tracking !== false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error refreshing flag date', dateStr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
saveMenuCache();
|
||||||
|
updateLastUpdatedTime(new Date().toISOString());
|
||||||
|
updateAlarmBell();
|
||||||
|
renderVisibleWeeks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAlarmBell() {
|
||||||
|
const bellBtn = document.getElementById('alarm-bell');
|
||||||
|
const bellIcon = document.getElementById('alarm-bell-icon');
|
||||||
|
if (!bellBtn || !bellIcon) return;
|
||||||
|
|
||||||
|
if (userFlags.size === 0) {
|
||||||
|
bellBtn.classList.add('hidden');
|
||||||
|
bellBtn.style.display = 'none';
|
||||||
|
bellIcon.style.color = 'var(--text-secondary)';
|
||||||
|
bellIcon.style.textShadow = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bellBtn.classList.remove('hidden');
|
||||||
|
bellBtn.style.display = 'inline-flex';
|
||||||
|
|
||||||
|
// Check if any flagged item is available
|
||||||
|
let anyAvailable = false;
|
||||||
|
for (const wk of allWeeks) {
|
||||||
|
if (!wk.days) continue;
|
||||||
|
for (const d of wk.days) {
|
||||||
|
if (!d.items) continue;
|
||||||
|
for (const item of d.items) {
|
||||||
|
if (item.available && userFlags.has(item.id)) {
|
||||||
|
anyAvailable = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (anyAvailable) break;
|
||||||
|
}
|
||||||
|
if (anyAvailable) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastUpdatedStr = localStorage.getItem('kantine_last_updated');
|
||||||
|
let timeStr = 'gerade eben'; // Fallback instead of Unbekannt
|
||||||
|
if (!lastUpdatedStr) {
|
||||||
|
lastUpdatedStr = new Date().toISOString();
|
||||||
|
localStorage.setItem('kantine_last_updated', lastUpdatedStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastUpdated = new Date(lastUpdatedStr);
|
||||||
|
const diffMs = Date.now() - lastUpdated.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
if (diffMins < 1) timeStr = 'gerade eben';
|
||||||
|
else if (diffMins < 60) timeStr = `vor ${diffMins} Min.`;
|
||||||
|
else timeStr = `vor ${Math.floor(diffMins / 60)} Std.`;
|
||||||
|
|
||||||
|
bellBtn.title = `Zuletzt geprüft: ${timeStr}`;
|
||||||
|
|
||||||
|
if (anyAvailable) {
|
||||||
|
bellIcon.style.color = '#10b981'; // green / success
|
||||||
|
bellIcon.style.textShadow = '0 0 10px rgba(16, 185, 129, 0.4)';
|
||||||
|
} else {
|
||||||
|
bellIcon.style.color = '#f59e0b'; // yellow / warning
|
||||||
|
bellIcon.style.textShadow = '0 0 10px rgba(245, 158, 11, 0.4)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleFlag(date, articleId, name, cutoff) {
|
function toggleFlag(date, articleId, name, cutoff) {
|
||||||
const id = `${date}_${articleId}`;
|
const id = `${date}_${articleId}`;
|
||||||
|
let flagAdded = false;
|
||||||
if (userFlags.has(id)) {
|
if (userFlags.has(id)) {
|
||||||
userFlags.delete(id);
|
userFlags.delete(id);
|
||||||
showToast(`Flag entfernt für ${name}`, 'success');
|
showToast(`Flag entfernt für ${name}`, 'success');
|
||||||
} else {
|
} else {
|
||||||
userFlags.add(id);
|
userFlags.add(id);
|
||||||
|
flagAdded = true;
|
||||||
showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
|
showToast(`Benachrichtigung aktiviert für ${name}`, 'success');
|
||||||
if (Notification.permission === 'default') {
|
if (Notification.permission === 'default') {
|
||||||
Notification.requestPermission();
|
Notification.requestPermission();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
saveFlags();
|
saveFlags();
|
||||||
|
updateAlarmBell();
|
||||||
renderVisibleWeeks();
|
renderVisibleWeeks();
|
||||||
|
|
||||||
|
if (flagAdded) {
|
||||||
|
refreshFlaggedItems();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FR-019: Auto-remove flags whose cutoff has passed
|
// FR-019: Auto-remove flags whose cutoff has passed
|
||||||
function cleanupExpiredFlags() {
|
function cleanupExpiredFlags() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const todayStr = now.toISOString().split('T')[0]; // Format: YYYY-MM-DD
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
for (const flagId of [...userFlags]) {
|
for (const flagId of [...userFlags]) {
|
||||||
const [date] = flagId.split('_');
|
const [dateStr] = flagId.split('_'); // Format usually is YYYY-MM-DD
|
||||||
const cutoff = new Date(date);
|
|
||||||
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00
|
// If the flag's date string is entirely in the past (before today)
|
||||||
if (now >= cutoff) {
|
// or if it's today but past the 10:00 cutoff time
|
||||||
|
let isExpired = false;
|
||||||
|
|
||||||
|
if (dateStr < todayStr) {
|
||||||
|
isExpired = true;
|
||||||
|
} else if (dateStr === todayStr) {
|
||||||
|
const cutoff = new Date(dateStr);
|
||||||
|
cutoff.setHours(10, 0, 0, 0); // Standard cutoff 10:00
|
||||||
|
if (now >= cutoff) {
|
||||||
|
isExpired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
userFlags.delete(flagId);
|
userFlags.delete(flagId);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
@@ -788,6 +1263,7 @@
|
|||||||
console.log(`[Cache] Parsed ${allWeeks.length} weeks:`, allWeeks.map(w => `KW${w.weekNumber}/${w.year} (${(w.days || []).length} days)`));
|
console.log(`[Cache] Parsed ${allWeeks.length} weeks:`, allWeeks.map(w => `KW${w.weekNumber}/${w.year} (${(w.days || []).length} days)`));
|
||||||
renderVisibleWeeks();
|
renderVisibleWeeks();
|
||||||
updateNextWeekBadge();
|
updateNextWeekBadge();
|
||||||
|
updateAlarmBell();
|
||||||
if (cachedTs) updateLastUpdatedTime(cachedTs);
|
if (cachedTs) updateLastUpdatedTime(cachedTs);
|
||||||
console.log('Loaded menu from cache');
|
console.log('Loaded menu from cache');
|
||||||
return true;
|
return true;
|
||||||
@@ -1000,6 +1476,7 @@
|
|||||||
updateAuthUI(); // This will trigger fetchOrders if logged in
|
updateAuthUI(); // This will trigger fetchOrders if logged in
|
||||||
renderVisibleWeeks();
|
renderVisibleWeeks();
|
||||||
updateNextWeekBadge();
|
updateNextWeekBadge();
|
||||||
|
updateAlarmBell();
|
||||||
|
|
||||||
progressMessage.textContent = 'Fertig!';
|
progressMessage.textContent = 'Fertig!';
|
||||||
setTimeout(() => progressModal.classList.add('hidden'), 500);
|
setTimeout(() => progressModal.classList.add('hidden'), 500);
|
||||||
@@ -1157,6 +1634,14 @@
|
|||||||
badge.classList.add('has-highlights');
|
badge.classList.add('has-highlights');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FR-092: Highlight Next Week Button when new data arrives
|
||||||
|
const storageKey = `kantine_notified_nextweek_${nextYear}_${nextWeek}`;
|
||||||
|
if (!localStorage.getItem(storageKey)) {
|
||||||
|
localStorage.setItem(storageKey, 'true');
|
||||||
|
btnNextWeek.classList.add('new-week-available');
|
||||||
|
showToast('Neue Menüdaten für nächste Woche verfügbar!', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
} else if (badge) {
|
} else if (badge) {
|
||||||
badge.remove();
|
badge.remove();
|
||||||
}
|
}
|
||||||
@@ -1541,7 +2026,12 @@
|
|||||||
: `${GITHUB_API}/releases?per_page=20`;
|
: `${GITHUB_API}/releases?per_page=20`;
|
||||||
|
|
||||||
const resp = await fetch(endpoint, { headers: githubHeaders() });
|
const resp = await fetch(endpoint, { headers: githubHeaders() });
|
||||||
if (!resp.ok) throw new Error(`GitHub API ${resp.status}`);
|
if (!resp.ok) {
|
||||||
|
if (resp.status === 403) {
|
||||||
|
throw new Error('API Rate Limit erreicht (403). Bitte später erneut versuchen.');
|
||||||
|
}
|
||||||
|
throw new Error(`GitHub API ${resp.status}`);
|
||||||
|
}
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
// Normalize to common format: { tag, name, url, body }
|
// Normalize to common format: { tag, name, url, body }
|
||||||
@@ -1617,19 +2107,8 @@
|
|||||||
const dm = devToggle.checked;
|
const dm = devToggle.checked;
|
||||||
container.innerHTML = '<p style="color:var(--text-secondary);">Lade Versionen...</p>';
|
container.innerHTML = '<p style="color:var(--text-secondary);">Lade Versionen...</p>';
|
||||||
|
|
||||||
try {
|
function renderVersionsList(versions) {
|
||||||
let versions;
|
if (!versions || !versions.length) {
|
||||||
const cached = JSON.parse(localStorage.getItem('kantine_version_cache') || 'null');
|
|
||||||
if (!forceRefresh && cached && cached.devMode === dm && (Date.now() - cached.timestamp < 3600000)) {
|
|
||||||
versions = cached.versions;
|
|
||||||
} else {
|
|
||||||
versions = await fetchVersions(dm);
|
|
||||||
localStorage.setItem('kantine_version_cache', JSON.stringify({
|
|
||||||
timestamp: Date.now(), devMode: dm, versions
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!versions.length) {
|
|
||||||
container.innerHTML = '<p style="color:var(--text-secondary);">Keine Versionen gefunden.</p>';
|
container.innerHTML = '<p style="color:var(--text-secondary);">Keine Versionen gefunden.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1661,6 +2140,34 @@
|
|||||||
`;
|
`;
|
||||||
list.appendChild(li);
|
list.appendChild(li);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Show cached versions immediately if available
|
||||||
|
const cachedRaw = localStorage.getItem('kantine_version_cache');
|
||||||
|
let cached = null;
|
||||||
|
if (cachedRaw) {
|
||||||
|
try { cached = JSON.parse(cachedRaw); } catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cached && cached.devMode === dm && cached.versions) {
|
||||||
|
renderVersionsList(cached.versions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch fresh versions in background (or foreground if no cache)
|
||||||
|
const liveVersions = await fetchVersions(dm);
|
||||||
|
|
||||||
|
// Compare with cache to see if we need to re-render
|
||||||
|
const liveVersionsStr = JSON.stringify(liveVersions);
|
||||||
|
const cachedVersionsStr = cached ? JSON.stringify(cached.versions) : '';
|
||||||
|
|
||||||
|
if (liveVersionsStr !== cachedVersionsStr) {
|
||||||
|
localStorage.setItem('kantine_version_cache', JSON.stringify({
|
||||||
|
timestamp: Date.now(), devMode: dm, versions: liveVersions
|
||||||
|
}));
|
||||||
|
renderVersionsList(liveVersions);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
container.innerHTML = `<p style="color:#e94560;">Fehler: ${e.message}</p>`;
|
container.innerHTML = `<p style="color:#e94560;">Fehler: ${e.message}</p>`;
|
||||||
}
|
}
|
||||||
|
|||||||
25
mock-data.js
25
mock-data.js
@@ -128,8 +128,31 @@
|
|||||||
// Orders endpoint
|
// Orders endpoint
|
||||||
if (urlStr.includes('/user/orders/') && (!options || options.method === 'GET' || !options.method)) {
|
if (urlStr.includes('/user/orders/') && (!options || options.method === 'GET' || !options.method)) {
|
||||||
console.log('[MOCK] Returning mock orders');
|
console.log('[MOCK] Returning mock orders');
|
||||||
|
// Formatter for history mapping
|
||||||
|
const mappedOrders = mockOrders.map(o => ({
|
||||||
|
id: o.id,
|
||||||
|
date: `${o.date}T10:00:00Z`,
|
||||||
|
order_state: o.status === 'cancelled' ? 9 : 5,
|
||||||
|
total: o.price || '6.50',
|
||||||
|
items: [{
|
||||||
|
article: o.article,
|
||||||
|
name: o.article_name,
|
||||||
|
price: o.price || '6.50',
|
||||||
|
amount: 1
|
||||||
|
}]
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Handle lazy load / pagination if requesting full history
|
||||||
|
if (urlStr.includes('limit=50')) {
|
||||||
|
return Promise.resolve(new Response(JSON.stringify({
|
||||||
|
count: mappedOrders.length,
|
||||||
|
next: null,
|
||||||
|
results: mappedOrders
|
||||||
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve(new Response(JSON.stringify({
|
return Promise.resolve(new Response(JSON.stringify({
|
||||||
results: mockOrders
|
results: mappedOrders
|
||||||
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
58
release.sh
Executable file
58
release.sh
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# release.sh - Deploys a new version of the Kantine Wrapper
|
||||||
|
|
||||||
|
# Ensure we're in the script directory
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Ensure tests have run and artifacts exist
|
||||||
|
if [ ! -d "$SCRIPT_DIR/dist" ]; then
|
||||||
|
echo "❌ Error: dist folder missing. Please run build-bookmarklet.sh first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get current version
|
||||||
|
VERSION=$(cat "version.txt" | tr -d '\n\r ')
|
||||||
|
|
||||||
|
# Validate that version is set
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "❌ Error: Could not determine version from version.txt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Kantine Bookmarklet Releaser ($VERSION) ==="
|
||||||
|
|
||||||
|
# Check for uncommitted changes (excluding dist/)
|
||||||
|
if ! git diff-index --quiet HEAD -- ":(exclude)dist"; then
|
||||||
|
echo "⚠️ Warning: You have uncommitted changes in the working directory."
|
||||||
|
echo "Please commit your code changes before running the release script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Committing build artifacts ==="
|
||||||
|
git add "dist/"
|
||||||
|
git commit -m "chore: update build artifacts for $VERSION" --allow-empty
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Tagging $VERSION ==="
|
||||||
|
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
||||||
|
git tag -f "$VERSION"
|
||||||
|
echo "🔄 Tag $VERSION moved to current commit."
|
||||||
|
else
|
||||||
|
git tag "$VERSION"
|
||||||
|
echo "✅ Created tag: $VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Pushing to remotes ==="
|
||||||
|
# Determine remote targets: Assume 'origin' for primary, optionally 'github'
|
||||||
|
git push origin HEAD
|
||||||
|
git push origin --force tag "$VERSION"
|
||||||
|
|
||||||
|
# If a remote named 'github' exists, push tags there too
|
||||||
|
if git remote | grep -q "^github$"; then
|
||||||
|
git push github --force tag "$VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🎉 Successfully released version $VERSION!"
|
||||||
|
exit 0
|
||||||
209
style.css
209
style.css
@@ -186,6 +186,32 @@ body {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Notification state for Next Week */
|
||||||
|
.nav-btn.new-week-available {
|
||||||
|
animation: goldPulse 2s infinite;
|
||||||
|
border-color: #f59e0b;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.new-week-available.active {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes goldPulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 10px rgba(245, 158, 11, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Badge for nav buttons (day count indicator) */
|
/* Badge for nav buttons (day count indicator) */
|
||||||
.nav-badge {
|
.nav-badge {
|
||||||
background-color: var(--error-color);
|
background-color: var(--error-color);
|
||||||
@@ -437,7 +463,6 @@ body {
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
/* Changed from --surface */
|
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -446,6 +471,174 @@ body {
|
|||||||
animation: modalSlide 0.3s ease-out;
|
animation: modalSlide 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* History Modal specific */
|
||||||
|
.history-modal-content {
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 85vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-modal-content .modal-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0;
|
||||||
|
/* Padding is handled by inner elements */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* History Styles */
|
||||||
|
.history-year-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-year-header {
|
||||||
|
background: var(--bg-card);
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-group {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 20px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-body);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-header:hover {
|
||||||
|
background: var(--border-color);
|
||||||
|
/* Slight hover effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-content {
|
||||||
|
display: none;
|
||||||
|
/* Collapsed by default */
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-group.open .history-month-content {
|
||||||
|
display: block;
|
||||||
|
/* Expanded when open class is present */
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-group.open .history-month-header .material-icons-round {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-month-header .material-icons-round {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-week-group {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-bottom: 1px dashed var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-week-group:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-week-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-week-summary {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background: rgba(100, 116, 139, 0.1);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50px 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--bg-body);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-date {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-name {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-price {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-status {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-cancelled {
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item-price-cancelled {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes modalSlide {
|
@keyframes modalSlide {
|
||||||
from {
|
from {
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
@@ -608,7 +801,7 @@ body {
|
|||||||
/* No opacity/filter here - fully visible */
|
/* No opacity/filter here - fully visible */
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
border: 1px solid var(--accent-color);
|
border: 1px solid #8b5cf6;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 0 -1rem 1.5rem -1rem;
|
margin: 0 -1rem 1.5rem -1rem;
|
||||||
@@ -617,8 +810,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.menu-item.today-ordered {
|
.menu-item.today-ordered {
|
||||||
border: 2px solid var(--accent-color);
|
border: 2px solid #8b5cf6;
|
||||||
box-shadow: 0 0 20px rgba(96, 165, 250, 0.4);
|
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 0 -1rem 1.5rem -1rem;
|
margin: 0 -1rem 1.5rem -1rem;
|
||||||
@@ -630,15 +823,15 @@ body {
|
|||||||
|
|
||||||
@keyframes pulse-glow {
|
@keyframes pulse-glow {
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 15px rgba(96, 165, 250, 0.3);
|
box-shadow: 0 0 15px rgba(139, 92, 246, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 25px rgba(96, 165, 250, 0.6);
|
box-shadow: 0 0 25px rgba(139, 92, 246, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 15px rgba(96, 165, 250, 0.3);
|
box-shadow: 0 0 15px rgba(139, 92, 246, 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1428,8 +1621,6 @@ body {
|
|||||||
.version-list {
|
.version-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-height: 350px;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ try {
|
|||||||
[/function\s+isNewer/, 'isNewer function'],
|
[/function\s+isNewer/, 'isNewer function'],
|
||||||
[/function\s+openVersionMenu/, 'openVersionMenu function'],
|
[/function\s+openVersionMenu/, 'openVersionMenu function'],
|
||||||
[/kantine_dev_mode/, 'dev-mode localStorage key'],
|
[/kantine_dev_mode/, 'dev-mode localStorage key'],
|
||||||
[/function\s+isCacheFresh/, 'isCacheFresh function']
|
[/function\s+isCacheFresh/, 'isCacheFresh function'],
|
||||||
|
[/limit=5/, 'Delta fetch limit parameter']
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [regex, label] of checks) {
|
for (const [regex, label] of checks) {
|
||||||
|
|||||||
49
tests/test_dom.js
Executable file
49
tests/test_dom.js
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const jsdom = require('jsdom');
|
||||||
|
const { JSDOM } = jsdom;
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
.icon-btn { display: inline-flex; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button id="alarm-bell" class="icon-btn hidden">
|
||||||
|
<span id="alarm-bell-icon" style="color:var(--text-secondary);"></span>
|
||||||
|
</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const jsCode = fs.readFileSync('kantine.js', 'utf8').replace('(function () {', '').replace(/}\)\(\);$/, '');
|
||||||
|
|
||||||
|
const dom = new JSDOM(html, { runScripts: "dangerously" });
|
||||||
|
global.window = dom.window;
|
||||||
|
global.document = window.document;
|
||||||
|
global.localStorage = { getItem: () => '[]', setItem: () => {} };
|
||||||
|
global.sessionStorage = { getItem: () => null };
|
||||||
|
|
||||||
|
global.showToast = () => {};
|
||||||
|
global.saveFlags = () => {};
|
||||||
|
global.renderVisibleWeeks = () => {};
|
||||||
|
// Mock missing browser features if needed
|
||||||
|
global.Notification = { permission: 'default', requestPermission: () => {} };
|
||||||
|
|
||||||
|
try {
|
||||||
|
dom.window.eval(jsCode);
|
||||||
|
console.log("Initial Bell Classes:", window.document.getElementById('alarm-bell').className);
|
||||||
|
|
||||||
|
// Add flag
|
||||||
|
dom.window.eval("userFlags.add('2026-02-24_123'); updateAlarmBell();");
|
||||||
|
console.log("After Add:", window.document.getElementById('alarm-bell').className);
|
||||||
|
|
||||||
|
// Remove flag
|
||||||
|
dom.window.eval("userFlags.delete('2026-02-24_123'); updateAlarmBell();");
|
||||||
|
console.log("After Remove:", window.document.getElementById('alarm-bell').className);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
v1.3.2
|
v1.4.16
|
||||||
|
|||||||
Reference in New Issue
Block a user