diff --git a/CHANGELOG.md b/CHANGELOG.md index 85208f5..a37d2da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,134 @@ Format guidelines: - Keep it concise: what is new, what changed/improved, what was fixed. - Avoid exhaustive UI/visual layout breakdowns. --> + +### v24.6.2 +- 🔄 **Geändert / Improved**: + - **Übersichtlichere Einstellungen / Clearer Settings**: Das schlecht lesbare native Menü wurde durch eine kontrastreiche, gruppierte Auswahl ersetzt. Die Startseite blendet Platzhalter-Firmendaten aus und bietet klarere Schnellzugriffe. + - **Mehrsprachige Navigation / Localized Navigation**: Tastaturbedienung und Einstellungen-Hilfe sind in Deutsch, Englisch, Französisch und Spanisch verfügbar. + +### v24.6.1 +- 🔄 **Geändert / Improved**: + - **Systemmodus als fester Standard / System Theme as the Default**: Die Ersteinrichtung wählt immer „System“ voraus, auch wenn zuvor ein anderer Firmenmodus gespeichert war. + - **Beschriebene App-Kacheln / Descriptive App Cards**: Apps werden mit Symbol, Kurzbeschreibung und klarer Auswahl dargestellt; alle Texte sind in vier Sprachen verfügbar. + - **Mehrsprachige Einrichtungs-Hilfe / Localized Onboarding Guide**: Die neue Ersteinrichtung ist im integrierten Handbuch erklärt. + - **Verifiziert / Verified**: Lint und Produktions-Build erfolgreich; die Vorschau wurde im folgenden Einstellungen-Update geprüft. + +### v24.6.0 +- 🔄 **Geändert / Improved**: + - **Einstellungen auf schmalen Fenstern / Settings on Narrow Windows**: Die abgeschnittene Kategorie-Leiste wurde durch eine kompakte, nach Bereichen gruppierte Auswahl ersetzt; Suchfeld, Übersichtskarten und Schnelloptionen passen sich an schmale Fenster an. + +### v24.5.0 +- 🚀 **Neu / What's New**: + - **Optionaler Einrichtungsassistent für neue Konten / Optional First-Account Setup**: Bei der ersten Kontoerstellung können Akzentfarbe, Hell-/Dunkel-/Systemmodus, ein lokales Hintergrundbild und passende App-Vorlagen gewählt werden. + - **App-Vorlagen / App Templates**: Privat, Geschäftlich, Praxis und Einzelhandel bieten passende Module wie Rechnungen, Buchhaltung, Lager, Einkauf und Kasse; Apps lassen sich zusätzlich einzeln auswählen. +- 🔄 **Geändert / Improved**: + - **Überspringen und später anpassen / Skip and personalize later**: Die Einrichtung ist vollständig optional; Live-Vorschauen werden beim Überspringen zurückgesetzt und gewählte Einstellungen dem neuen lokalen Konto zugeordnet. + - **Mehrsprachige Einrichtung / Quad-Language Setup**: Alle neuen Texte sind auf Deutsch, Englisch, Französisch und Spanisch verfügbar. + +### v24.4.0 +- 🚀 **Neu / What's New**: + - **Word (.docx) Tabellen-Engine für mehrseitige Dokumente / Word (.docx) Multi-Page Table Engine**: Vollständige XML-AST-Analyse für komplexe Word-Tabellen mit vertikaler (`w:vMerge`) und horizontaler (`w:gridSpan`) Zellverschmelzung, Unterdrückung von Seitenumbrüchen innerhalb von Tabellenzeilen (`w:cantSplit`) und verschachtelten Tabellen. + - **Lokale Vorlagenüberwachung / Desktop Templates Folder Watcher**: Automatische Echtzeit-Überwachung des Vorlagenordners `%APPDATA%/socdof/templates` (oder portablem `templates/`-Verzeichnis). Neue oder geänderte Word-, PDF- und HTML-Vorlagendateien werden sofort in der Oberfläche angezeigt und können mit einem Klick importiert werden. + - **Interaktiver Briefpapier-Editor mit visueller Feldplatzierung / PDF Stationery Visual Placement Studio**: Belegdaten (Rechnungsnummer, Datum, Kunde, Betrag, Positionen) lassen sich auf einem interaktiven DIN-A4-Canvas per Drag-and-Drop direkt auf grafischem Firmenbriefpapier oder PDF-Hintergründen positionieren. + - **Hochauflösende PDF-Briefpapier-Konvertierung**: Hochgeladene PDF-Briefpapiere werden automatisch mit gestochen scharfer 2-facher Druckauflösung via `pdfjs-dist` direkt im Browser/Electron in Hintergrundgrafiken konvertiert. +- 🔄 **Geändert / Improved**: + - **Vorlagen-Manager mit Live-Synchronisation**: Übersichtlicher Desktop-Vorlagenbereich mit Statusanzeige („Auto-Sync aktiv“), Explorer-Schaltfläche und Schnellimport. + +### v24.3.0 +- 🚀 **Neu / What's New**: + - **Standardmäßiger Randlos-Vollbildmodus / Default Borderless Fullscreen**: SOCDOF startet nun standardmäßig automatisch im randlosen Vollbildmodus bis zu den Bildschirmkanten und verdeckt die Windows-Taskleiste vollständig – für maximale Arbeitsfläche und Fokus. + - **Obere Windows-Fenstersteuerung / Top Window Controls**: Oben rechts befindet sich die vertraute Windows-Fenstersteuerung mit Sofort-Buttons für Fenster minimieren (`-`), Verkleinern/Vollbildmodus (`🗖` / `🗗` via F11) sowie Schließen (`✕`). Kein störender Logout-Button in der oberen Leiste. + - **Universelles F11 & Fenster-Minimieren / Global F11 & Window Minimize**: F11 schaltet überall nahtlos zwischen Vollbild und Fenstermodus ("kleiner machen") um. Das Minimieren wird über die native Electron-Schnittstelle sowie die Web-Fullscreen-API unterstützt. + - **Einstellungs-Schalter für Vollbild-Start**: Unter *Einstellungen > Windows* lässt sich der automatische Vollbildstart mit einem Klick aktivieren oder deaktivieren – mit voller Unterstützung für Hell- und Dunkelmodus. + - **Vollständige Akzent- & Kontrast-Harmonisierung / Complete Suite Dynamic Accent Harmonization**: Sämtliche Module (Kontakte & CRM, Lager & Produkte, Einkauf & Beschaffung, Support & Zeiterfassung, Praxis & Therapie, Bug-Reports & Feedback, Einstellungen) sind nun vollständig auf das dynamische Akzentfarbensystem (`--accent`, `--accent-light`, `--accent-companion`) abgestimmt. +- 🔄 **Geändert / Improved**: + - **Volle Light-Mode-Parität / Full Light Mode Parity**: Die oberen Fenstersteuerungen, Schalter, Tab-Leisten und Module bieten im Hell- und Dunkelmodus perfekte Kontraste und Lesbarkeit. + - **Sperr- & Anmeldebildschirm Fenstersteuerung**: Auch im Login- und Lock-Screen stehen die Tastensteuerung (F11) sowie obere Minimieren- und Vollbild-Buttons direkt zur Verfügung. + +### v24.0.0 +- 🚀 **Neu / What's New**: + - **Handy- & Kompaktfenster-Layout / Mobile-Window Responsive System**: Zieht man ein Fenster in SOCDOF auf eine schmale Smartphone-Breite oder nutzt die App auf Touchscreens, schaltet das Layout automatisch auf ein optimiertes Handy-Layout um. + - **Mobiles Login & Sperrbildschirm / Mobile-Optimized Auth & Lock Screen**: Der Windows 11 Sperrbildschirm, die Benutzer-Auswahl und der Anmeldedialog passen sich nahtlos an schmale Bildschirme und Handys an – mit optimierter Uhr-Typografie, One-Touch-Wischgeste nach oben und responsiven Touch-Schaltflächen (mind. 44px). + - **Mobile Kartenansicht für Rechnungen, Produkte, Einkauf & Kontakte**: In schmalen Fenstern schalten Daten-Tabellen automatisch auf übersichtliche Touch-Karten mit Direktaktionen (PDF Drucken, Bezahlen, Buchen, Anrufen, E-Mail) um. + - **Mobile POS-Kassenansicht mit Express-Checkout-Leiste**: Ermöglicht schnelles Umschalten zwischen Produktkatalog und Warenkorb sowie eine 1-Tap Kassenabwicklung auf kompakten Bildschirmen. +- 🛠️ **Behoben / Fixed**: + - **React Render-Cycle Synchronisation behoben / Clean Effect State Updates**: Behebt eine Warnung (`Cannot update a component while rendering a different component`), indem Appearance- und Sprach-Initialisierungen (`setLanguage`, `applyAccentColor`) in `AccountScopedWorkspace` strikt in `useEffect`-Hooks ausgeführt werden und redundante Listener-Trigger vermieden werden. + - **Kollision mit virtueller Bildschirmtastatur auf Smartphones behoben / Mobile Keyboard Collision Fix**: Wenn auf Handys das Passwort-Eingabefeld angetippt wird und die Bildschirmtastatur hochklappt, überlappt der „Passwort vergessen?“-Link nicht mehr mit der Benutzerleiste. Das gesamte Anmeldefenster nutzt ein flexibles `min-h-[100dvh]`-Layout mit sanftem Scrollbereich. + - **Keine horizontale Überbreite mehr auf Mobilgeräten / Mobile Viewport Auto-Fit**: Beim Öffnen von Fenstern auf Smartphones und schmalen Bildschirmen (< 768px) passen sich Fenster automatisch auf 100% Bildschirmbreite und -höhe an, anstatt auf 920px Desktopbreite zu klemmen. +- 🔄 **Geändert / Improved**: + - **Touch-Freundlichkeit & Formular-Skalierung**: Sämtliche Authentifizierungs- und Eingabefelder im Sperrbildschirm skalieren ohne horizontales Abschneiden auf 100% Breite mit verbesserter Tastaturbedienung. + +### v23.19.1 +- 🚀 **Neu / What's New**: + - **Intelligente Kontrast-Begleitfarbe für Diagramme & Doppelbalken / Smart Anti-Clash Companion Colors**: Wenn zwei Vergleichsdaten nebeneinander dargestellt werden (z. B. Umsatz vs. Sitzungen in Praxis & Therapie oder bezahlt vs. offen im Dashboard), berechnet das System automatisch eine harmonische, kontrastierende Begleitfarbe (`--accent-companion`). Wählt der Nutzer z. B. Grün/Smaragd als Akzentfarbe, wechselt die zweite Serie automatisch zu einem eleganten Indigo-Violett statt ebenfalls grün zu sein. Bei Orange/Gold wechselt sie zu Cyan/Türkis. Zwei Datenreihen sehen dadurch niemals mehr gleich aus. + - **Dezenter Akzentfarben-Glow auf Karten & Umrandungen / Subtle Ambient Accent Glow**: Alle Apps und Module (Praxis & Therapie, Rechnungen, POS Kasse, Kundenbuch, Lager, BWA, Kalender etc.) besitzen nun auf allen Karten, Containern und Umrandungen einen dezenten, edlen Schimmer in der in den Einstellungen gewählten Akzentfarbe. Beim Hovern hebt sich der Glow sanft an, und fokussierte Formularfelder leuchten in einem passenden Akzentring – DIN-Ausdrucke bleiben garantiert unbeeinflusst. + - **Unabhängig scrollbare Einstellungsleiste & Inhaltsbereich / Independent Dual-Scroll Layout**: Die linke Navigationsleiste der Einstellungen und der rechte Inhaltsbereich besitzen nun jeweils eigene, unabhängige Scrollbereiche (`overflow-y-auto`). Beim Durchscrollen umfangreicher Einstellungsseiten bleibt die linke Menüleiste stets sichtbar und kann separat bedient werden, ohne dass die andere Seite mitverschoben wird. + - **Tastenkombinationen & Kürzel-Tab in den Einstellungen / Keyboard Shortcuts Hub**: Neuer Bereich in den Einstellungen (*System & Personalisierung*), der alle globalen Shortcuts der App übersichtlich und kategorisiert anzeigt (Feedback, Systemnavigation, Taskleiste, virtuelle Desktops, Sicherheit) – inklusive Echtzeit-Tastatursuche und interaktiver „Jetzt testen“-Schaltflächen. + - **Sofortige Melde-Hotkeys für Feedback / Global Reporting Hotkeys**: + - `Alt + B`: Öffnet sofort das Bug-Meldefenster mit vorbefüllten Diagnosedaten von überall in der App. + - `Alt + I`: Öffnet direkt das Eingabefenster für Feature-Ideen und Vorschläge. + - `Alt + R`: Direkter Aufruf des Feedback- und Ticket-Verlaufs. + - **Dynamische Akzentfarben-Harmonie & Desktop-Aura / Dynamic Accent Color Aura**: Die in den Einstellungen gewählte Akzentfarbe (z. B. Indigo, Aubergine, Cyber Blue, Smaragdgrün, Sunset Gold oder benutzerdefinierte Picasso-Farbe) wird nun lebendig und geschmackvoll im gesamten Desktop integriert: aktive Fenster mit 2,5px oberer Akzentlinie, dezentem Titelbalken-Verlauf, Icon-Ringen, Aero-Snap-Docks, Spotlight-Auswahl und sanfter Mica-Desktop-Aura. + - **Live-Vorschaukarte in der Personalisierung / Live Accent Preview**: Interaktive Miniatur-Vorschaukarte in den Farbeinstellungen, die Fensterleiste, Primäraktionen und Badges in der gewählten Akzentfarbe in Echtzeit demonstriert. +- 🛠️ **Behoben / Fixed**: + - **Entfernung des unscharfen Farbnebels am oberen Fensterrand / Clean Header & Titlebar Clarity**: Der störende radiale Farbverlauf oberhalb der Navigationsleiste sowie Titelbalken-Farbverläufe wurden vollständig entfernt. Der Fenster-Header und die Navigationsleiste sind nun kristallklar, sauber und blendfrei – der Akzentfarben-Glow wirkt präzise und elegant ausschließlich auf den tatsächlichen Kanten und Karten. + - **Dauerhafte Speicherung der Schriftgröße / Persistent Font Scaling**: Änderungen am Schriftgrößen-Regler (90% bis 130%) und 100%-Reset werden nun sofort und dauerhaft im lokalen Speicher (`localStorage`) sowie in der Datenbank gesichert. Auch nach einem Neustart oder Schließen des Fensters bleibt die gewählte Skalierung verlässlich erhalten – ohne Flackern beim Laden. +- 🔄 **Geändert / Improved**: + - **Augenfreundliche Arbeitsfläche / Ergonomic Surface Warmth**: Ablösung von grellem Reinweiß durch sanfte, ruhige Oberflächenkontraste (`#f6f8fb`), die die Augen bei langen Arbeitstagen schonen. + - **Kollisionsfreie Tastenerkennung / Safe Key Isolation**: Globale Tastenkombinationen sind sauber gegen AltGr-Tastenbelegungen isoliert, sodass Texteingaben in Formularen ungestört bleiben. + +### v23.18.13 +- 🛠️ **Behoben / Fixed**: + - **Direkte Windows-Übertragung ohne Localhost-Abhängigkeit / Direct Windows HTTPS Dispatch**: In der Windows-Desktop-App (`.exe`) werden Berichte nun über den Electron-Hauptprozess direkt per nativem HTTPS an `api.botghost.com` gesendet – völlig unabhängig von lokalen Webservern, Ports oder Localhost-Diensten. + - **Echte Sendebestätigung für Tickets / Verified Delivery Status**: Ein Ticket wird in der Ticketliste nur dann als „Erfolgreich gesendet“ markiert, wenn BotGhost den Empfang mit `HTTP 200` bestätigt hat. Bei Netzwerkfehlern oder Serverausfall wird der Bericht sicher in der Warteschlange abgelegt und transparent als offline gekennzeichnet. +- 🚀 **Neu / What's New**: + - **BotGhost Projekt- & Kategorie-Filtervariablen / BotGhost Filter Variables**: Jeder Bericht übermittelt nun automatisch `{webhook.project}` (`SOCDOF`) sowie `{webhook.report_category}` (`bug` bzw. `idea`), womit Berichte in BotGhost präzise in die passenden Kanäle sortiert werden können. + +### v23.18.12 +- 🛠️ **Behoben / Fixed**: + - **Echte Discord-Übertragung & Fehlerdiagnose / Verified Discord Bot Transmission**: Fehlerberichte und Feedback aus der App werden nun über den integrierten Localhost-Proxy und die offizielle Discord-Bot-API direkt als Forum-Threads im Discord-Server erstellt. + - **Keine Scheinerfolgsmeldungen mehr / Zero False Positives**: Ein Ticket meldet nun nur dann „Erfolgreich gesendet“, wenn Discord den Thread mit HTTP 200/201 bestätigt hat. Bei fehlendem Token oder Offline-Zustand wird der Grund transparent angezeigt und das Ticket lokal als Warteschlange markiert. +- 🚀 **Neu / What's New**: + - **Bot-Token Konfigurationsleiste / In-App Discord Bot Setup**: Im Kopfbereich des Bug-Report-Fensters kann der Discord-Bot-Token direkt hinterlegt und auf Gültigkeit geprüft werden. + +### v23.18.11 +- 🛠️ **Behoben / Fixed**: + - **Scroll-Blockade im Handbuch behoben / Handbuch Smooth Scrolling**: Das Dokumentationsportal reagiert nun wieder einwandfrei auf Mausrad und Scrollleisten. Die innere Flexbox-Höhe wurde korrigiert (`h-full min-h-0`), sodass keine Inhalte mehr unterhalb des Fensterrands festhängen. + - **Deplatzierte Karte „Praxis & Therapie“ entfernt / Removed Misplaced Header Card**: Die versehentlich oberhalb der Titelleiste platzierte Einzelkarte wurde entfernt. Das Portal startet nun wieder sauber mit der Standard-Kopfzeile und Version. +- 🚀 **Neu / What's New**: + - **Saubere Modul-Integration für Praxis & Therapie / Proper Module Chapter & Showcase**: Das Modul „Praxis & Therapie“ ist nun als eigenständiges Handbuch-Kapitel mit Workflow-Anleitung sowie als Modul-Kachel in der Showcase-Übersicht ordentlich integriert. + +### v23.18.10 +- 🔄 **Geändert / Improved**: + - **Exklusiver Release-Broadcast über Discord-Bot / Exclusive Discord Bot Release Broadcast**: Die alten Webhooks wurden vollständig aus den GitHub-Actions entfernt und deaktiviert. Release-Changelogs werden nun verlässlich und direkt über die offizielle Discord-Bot-API in den definierten Forum-Post übertragen. + - **Fehlerbehandlung bei fehlenden Secrets / Fail-Fast Configuration Validation**: Fehlen `DISCORD_BOT_TOKEN` oder `DISCORD_THREAD_ID` in den GitHub-Secrets, bricht das Skript kontrolliert mit einer klaren Fehlerdiagnose ab, anstatt unerwünschte Webhooks aufzurufen. + +### v23.18.9 +- 🚀 **Neu / What's New**: + - **Adaptives Bildschirm-Layout für Rechnungsvorlagen / Adaptive Screen Layout**: Auf kompakten Bildschirmen (< 1280px) ermöglicht eine schlanke Umschaltleiste den nahtlosen Wechsel zwischen „Layout-Editor“ und „Dokument-Vorschau“. Auf großen Bildschirmen (>= 1280px) werden beide Bereiche übersichtlich nebeneinander dargestellt, während die Leiste automatisch ausgeblendet wird. + - **Zoom-Steuerung für Beleg-Vorschau / Preview Zoom Controls**: Interaktives Stufen-Zooming (60% bis 130%) mit 100%-Reset für eine optimale Dokumentenansicht auf jeder Bildschirmauflösung. +- 🔄 **Geändert / Improved**: + - **Discord-Bot-Unterstützung für Forum-Posts / Discord Bot Forum Posts**: Die Release-GitHub-Actions unterstützen nun das automatische Posten direkt in einen spezifischen Discord-Forum-Thread über einen Discord-Bot-Token inklusive automatischem Reaktivieren (Unarchive) archivierter Threads. +- 🛠️ **Behoben / Fixed**: + - **Dark-Mode-Korrekturen im Auswahlmenü / Selection Menu Dark Mode Fixes**: Austausch nativer Browser-Dropdowns durch stilvolle, themengerechte Auswahlelemente mit Lucide-Icons. Dadurch werden unlesbare hellblaue Optionsmenüs mit weißer Schrift in dunklen Designs vollständig verhindert. + - **Originalgetreue DIN A4 Papier-Vorschau / Protected Paper Sheet**: Verhinderung von ungewollten Farb-Invertierungen des Belegbogens im Dark Mode für ein realistisches, druckgetreues Erscheinungsbild. + +### v23.18.8 +- 🚀 **Neu / What's New**: + - **Präsenter Lade-Indikator beim Senden / Prominent Transmission Banner**: Beim Klick auf „Senden“ erscheint sofort ein animierter Lade-Statusbalken („Wird an Discord gesendet... Bitte warten“) mit Spinner, während alle Eingaben gesperrt werden, um doppeltes Senden zu verhindern. +- 🛠️ **Behoben / Fixed**: + - **Echte Discord-Erfolgsbestätigung / Verified Live Delivery**: Erst wenn der Beitrag tatsächlich und nachweislich auf Discord veröffentlicht wurde, erscheint die grüne „Gesendet!“-Erfolgsmeldung. Bei Verbindungsabbrüchen wird transparent über die Offline-Warteschlange informiert und bei Fehlern eine detaillierte Fehlermeldung angezeigt. + +### v23.18.7 +- 🔄 **Geändert / Improved**: + - **Wiederherstellung des alten Webhooks / Restored Previous Webhook**: Rücksetzung der BotGhost Webhook-URL und Standard-Token-Einstellungen auf den vorherigen Zustand (`t5dcd2k8x1n8i53932gf`). + +### v23.18.6 +- 🚀 **Neu / What's New**: + - **Neuer BotGhost Webhook & API Token / Updated Webhook URL & Token**: Aktualisierung der BotGhost Webhook-URL (`9mvtuh5aaf65v4ipqlilqu`) und des BG API-Tokens (`17450aaada2fde267b22f9f917094d13e38c8ba7b51a4df047719f0fd1877089`) für reibungslose und authentifizierte Discord-Berichte. + +### v23.18.5 +- 🛠️ **Behoben / Fixed**: + - **Senden von Reports in der Desktop-App (Electron) / Desktop App Report Dispatch**: Berichte aus der Windows-Desktop-App heraus schlugen fehl, da Electron-Renderer-Anfragen an externe Webhooks blockiert wurden. + - **Node.js HTTPS Webhook IPC Bridge & net.fetch / Electron IPC Webhook Bridge**: Hinzufügen des IPC-Handlers `socdof:discord-webhook` in `electron/main.cjs` mittels `net.fetch` und nativem Node.js `https`-Modul (inklusive automatischem Redirect-Handling). Damit senden Bug- und Feature-Reports aus der installierten Desktop-App heraus zuverlässig und ohne Blockaden an Discord. diff --git a/package.json b/package.json index 6b465cd..6c16fb8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "socdof", "private": true, - "version": "24.4.0", + "version": "24.6.2", "description": "Strudel's Organization, Commerce & Documentation Offline Flow - Offline-first Windows ERP Desktop Suite & Web Workspace", "author": "Yuri / Strudel", "publisher": "Yuri / Strudel", diff --git a/src/App.tsx b/src/App.tsx index 18e9fc6..4aa2eb9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Contact, Product, @@ -121,11 +121,8 @@ export default function App() { if (session && !session.locked) { const user = getUserById(session.userId); if (user) { - const media = window.matchMedia('(prefers-color-scheme: dark)'); - const enableDark = nextMode === 'dark' || (nextMode !== 'light' && media.matches); - const userTheme = enableDark ? 'dark' : 'light'; - if (user.preferences.theme !== userTheme) { - updateUserPreferences(user.id, { ...user.preferences, theme: userTheme }); + if (user.preferences.theme !== nextMode) { + updateUserPreferences(user.id, { ...user.preferences, theme: nextMode }); } } } @@ -397,11 +394,8 @@ export default function App() { if (session && !session.locked) { const user = getUserById(session.userId); if (user) { - const media = window.matchMedia('(prefers-color-scheme: dark)'); - const enableDark = updated.theme_mode === 'dark' || (updated.theme_mode !== 'light' && media.matches); - const userTheme = enableDark ? 'dark' : 'light'; - if (user.preferences.theme !== userTheme) { - updateUserPreferences(user.id, { ...user.preferences, theme: userTheme }); + if (user.preferences.theme !== updated.theme_mode) { + updateUserPreferences(user.id, { ...user.preferences, theme: updated.theme_mode }); } } } diff --git a/src/components/AccountScopedWorkspace.tsx b/src/components/AccountScopedWorkspace.tsx index b6f9c30..f300c13 100644 --- a/src/components/AccountScopedWorkspace.tsx +++ b/src/components/AccountScopedWorkspace.tsx @@ -129,6 +129,15 @@ function applyUserAppearance(user: UserAccount | null): void { } catch { // ignore } + } else if (user.preferences.theme === 'system') { + const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + root.classList.toggle('dark', isDark); + root.style.colorScheme = isDark ? 'dark' : 'light'; + try { + localStorage.removeItem('odoo_theme_dark'); + } catch { + // ignore + } } // ACCENT COLOR: If the user account explicitly has an accent color preference, apply it. // Otherwise, respect the saved system / company / localStorage accent color! DO NOT force 'indigo'! @@ -162,20 +171,18 @@ export const AccountScopedWorkspace: React.FC = ({ chil const account = session && !session.locked ? getUserById(session.userId) : null; setActiveAccount(account); }; - const onAccentOrCompanyChanged = () => { + const onCompanyChanged = () => { const session = getSession(); const account = session && !session.locked ? getUserById(session.userId) : null; applyUserAppearance(account); }; window.addEventListener(AUTH_CHANGE_EVENT_NAME, refreshAccount); window.addEventListener('storage', refreshAccount); - window.addEventListener('socdof-company-updated', onAccentOrCompanyChanged); - window.addEventListener('socdof-accent-changed', onAccentOrCompanyChanged); + window.addEventListener('socdof-company-updated', onCompanyChanged); return () => { window.removeEventListener(AUTH_CHANGE_EVENT_NAME, refreshAccount); window.removeEventListener('storage', refreshAccount); - window.removeEventListener('socdof-company-updated', onAccentOrCompanyChanged); - window.removeEventListener('socdof-accent-changed', onAccentOrCompanyChanged); + window.removeEventListener('socdof-company-updated', onCompanyChanged); }; }, []); diff --git a/src/components/AuthGate.tsx b/src/components/AuthGate.tsx index 3a68b6b..928a7c8 100644 --- a/src/components/AuthGate.tsx +++ b/src/components/AuthGate.tsx @@ -3,6 +3,25 @@ import { ArrowRight, Building2, Check, + BriefcaseBusiness, + ShoppingCart, + Calculator, + CalendarDays, + ChartNoAxesCombined, + ContactRound, + Download, + FileText, + CreditCard, + HeartPulse, + Layers, + LayoutGrid, + LifeBuoy, + MessageSquare, + Image as ImageIcon, + Moon, + Monitor, + Palette, + Sun, CheckCircle2, Eye, EyeOff, @@ -49,7 +68,8 @@ import { type UserRole, RECOVERY_QUESTIONS } from '../lib/auth'; -import { applyAccentColor } from '../lib/accent'; +import { ACCENT_LIST, applyAccentColor } from '../lib/accent'; +import type { ActiveModule } from '../types'; import { SUPPORTED_LANGUAGES, formatSystemDate, @@ -145,6 +165,80 @@ const getAuthCopy = (lang: LanguageCode) => ({ type AuthText = ReturnType; const fieldClass = 'w-full rounded-xl border border-slate-200/80 dark:border-white/10 bg-white/80 dark:bg-slate-900/70 text-slate-900 dark:text-white scheme-light dark:scheme-dark px-4 py-3 sm:py-2.5 text-base sm:text-sm outline-none focus:ring-2 focus:ring-indigo-500/40 min-h-[44px] transition'; const avatars = ['●', '◆', '▲', '■', '✦', '✚', '◉', '⬢']; +const SETUP_CORE_MODULES: ActiveModule[] = ['dashboard', 'appstore', 'docs', 'settings']; +const SETUP_MODULE_TEMPLATES: Record = { + personal: ['contacts', 'calendar', 'calculator', 'widgets'], + business: ['invoices', 'accounting', 'contacts', 'products', 'stock', 'purchases', 'calendar', 'calculator'], + practice: ['therapy_practice', 'contacts', 'calendar', 'invoices', 'accounting'], + retail: ['pos', 'products', 'stock', 'purchases', 'accounting', 'contacts', 'invoices'], +}; +const SETUP_TEMPLATE_KEYS = ['personal', 'business', 'practice', 'retail'] as const; +const SETUP_BUSINESS_ONLY_MODULES = new Set([ + 'invoices', 'accounting', 'purchases', 'pos', 'support_services' +]); +const SETUP_MODULE_OPTIONS: ActiveModule[] = [ + 'invoices', 'accounting', 'contacts', 'products', 'stock', 'purchases', 'calendar', 'calculator', + 'therapy_practice', 'support_services', 'pos', 'widgets', 'feedback', +]; +const SETUP_MODULE_ICONS: Partial>> = { + invoices: FileText, + accounting: ChartNoAxesCombined, + contacts: ContactRound, + products: BriefcaseBusiness, + stock: Layers, + purchases: Download, + calendar: CalendarDays, + calculator: Calculator, + therapy_practice: HeartPulse, + support_services: LifeBuoy, + pos: CreditCard, + widgets: LayoutGrid, + feedback: MessageSquare, +}; +const SETUP_MODULE_DESCRIPTIONS: Partial> = { + invoices: 'auth.setupModuleDesc.invoices', + accounting: 'auth.setupModuleDesc.accounting', + contacts: 'auth.setupModuleDesc.contacts', + products: 'auth.setupModuleDesc.products', + stock: 'auth.setupModuleDesc.stock', + purchases: 'auth.setupModuleDesc.purchases', + calendar: 'auth.setupModuleDesc.calendar', + calculator: 'auth.setupModuleDesc.calculator', + therapy_practice: 'auth.setupModuleDesc.therapy_practice', + support_services: 'auth.setupModuleDesc.support_services', + pos: 'auth.setupModuleDesc.pos', + widgets: 'auth.setupModuleDesc.widgets', + feedback: 'auth.setupModuleDesc.feedback', +}; + +function getSetupModulesForAccount(modules: ActiveModule[], accountType: AccountType): ActiveModule[] { + return accountType === 'personal' + ? modules.filter((module) => !SETUP_BUSINESS_ONLY_MODULES.has(module)) + : modules; +} + +function resizeOnboardingWallpaper(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = () => reject(new Error('image_read_failed')); + reader.onload = () => { + const image = new Image(); + image.onerror = () => reject(new Error('image_decode_failed')); + image.onload = () => { + const ratio = Math.min(1, 1920 / image.width, 1080 / image.height); + const canvas = document.createElement('canvas'); + canvas.width = Math.round(image.width * ratio); + canvas.height = Math.round(image.height * ratio); + const context = canvas.getContext('2d'); + if (!context) return reject(new Error('image_canvas_failed')); + context.drawImage(image, 0, 0, canvas.width, canvas.height); + resolve(canvas.toDataURL('image/webp', 0.82)); + }; + image.src = String(reader.result); + }; + reader.readAsDataURL(file); + }); +} function computePasswordStrength(password: string): { score: number; @@ -294,7 +388,7 @@ export function AuthGate({ children, company }: { children: React.ReactNode; com } if (currentUser.mustChangePassword) return ; if (locked) return ; - return
{children}
; + return
{children}
; } function LanguageSelectionScreen({ @@ -415,12 +509,12 @@ function LanguageSelectionScreen({ }; return ( -
-
+
+
{/* Header with icon and step badge */} -
-
-
+
+
+
{isFirstRunOnboarding && ( @@ -440,7 +534,7 @@ function LanguageSelectionScreen({ {/* Search bar when over 10 languages are present */} {availableLanguages.length > 10 && ( -
+
+
{filteredLanguages.map((language) => { const isSelected = selectedId === language.id; return ( @@ -477,7 +571,7 @@ function LanguageSelectionScreen({ : 'border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/5 hover:border-slate-300 dark:hover:border-white/20' }`} > -
+
{language.flagImage ? ( ) : language.emoji && language.emoji.length > 0 && language.emoji !== '🏳️' ? ( @@ -503,7 +597,7 @@ function LanguageSelectionScreen({ ); })} {filteredLanguages.length === 0 && ( -
+

{t('lang_modal.no_search_results', lang, 'Keine passende Sprache gefunden')} @@ -543,6 +637,19 @@ function FirstAccount({ }) { const lang = useLanguage(); const fileInputRef = useRef(null); + const wallpaperInputRef = useRef(null); + const [showInitialSetup, setShowInitialSetup] = useState(true); + const [setupAccent, setSetupAccent] = useState(() => initialCompany?.accent_color || (typeof localStorage !== 'undefined' ? localStorage.getItem('socdof_accent_color') : null) || 'indigo'); + const [setupTheme, setSetupTheme] = useState<'light' | 'dark' | 'system'>('system'); + const [setupWallpaper, setSetupWallpaper] = useState(() => initialCompany?.desktop_wallpaper_url || ''); + const [setupApps, setSetupApps] = useState(() => [...SETUP_MODULE_TEMPLATES.personal]); + const previewBaselineRef = useRef({ + isDark: typeof document !== 'undefined' && document.documentElement.classList.contains('dark'), + colorScheme: typeof document !== 'undefined' ? document.documentElement.style.colorScheme : '', + storedAccent: typeof localStorage !== 'undefined' ? localStorage.getItem('socdof_accent_color') : null, + accent: initialCompany?.accent_color || (typeof localStorage !== 'undefined' ? localStorage.getItem('socdof_accent_color') : null) || 'indigo', + }); + const keepSetupPreviewRef = useRef(false); const [form, setForm] = useState({ username: '', displayName: '', @@ -573,6 +680,86 @@ function FirstAccount({ const strength = computePasswordStrength(form.password); + const restoreSetupPreview = () => { + const root = document.documentElement; + const baseline = previewBaselineRef.current; + applyAccentColor(baseline.accent); + if (baseline.storedAccent === null) { + try { + localStorage.removeItem('socdof_accent_color'); + } catch { + // Ignore storage failures while restoring the preview. + } + } + root.classList.toggle('dark', baseline.isDark); + root.style.colorScheme = baseline.colorScheme; + }; + + useEffect(() => { + return () => { + if (!keepSetupPreviewRef.current) restoreSetupPreview(); + }; + }, []); + + useEffect(() => { + if (!showInitialSetup || setupTheme !== 'system') return; + const media = window.matchMedia('(prefers-color-scheme: dark)'); + const applySystemPreview = () => { + const isDark = media.matches; + document.documentElement.classList.toggle('dark', isDark); + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; + }; + applySystemPreview(); + media.addEventListener?.('change', applySystemPreview); + return () => media.removeEventListener?.('change', applySystemPreview); + }, [showInitialSetup, setupTheme]); + + const handleThemePreview = (mode: 'light' | 'dark' | 'system') => { + setSetupTheme(mode); + if (!showInitialSetup) return; + const isDark = mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); + document.documentElement.classList.toggle('dark', isDark); + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; + }; + + const handleAccentPreview = (accent: string) => { + setSetupAccent(accent); + if (showInitialSetup) applyAccentColor(accent); + }; + + const toggleInitialSetup = () => { + const nextEnabled = !showInitialSetup; + setShowInitialSetup(nextEnabled); + if (nextEnabled) { + applyAccentColor(setupAccent); + const isDark = setupTheme === 'dark' || (setupTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); + document.documentElement.classList.toggle('dark', isDark); + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; + } else { + restoreSetupPreview(); + } + }; + + const handleSetupWallpaperFile = async (file?: File) => { + if (!file?.type.startsWith('image/')) return; + try { + setSetupWallpaper(await resizeOnboardingWallpaper(file)); + setError(''); + } catch { + setError(t('auth.setupWallpaperError', lang)); + } + }; + + const selectAppTemplate = (template: keyof typeof SETUP_MODULE_TEMPLATES) => { + setSetupApps(getSetupModulesForAccount(SETUP_MODULE_TEMPLATES[template], form.accountType)); + }; + + const toggleSetupApp = (module: ActiveModule) => { + setSetupApps((current) => current.includes(module) + ? current.filter((item) => item !== module) + : [...current, module]); + }; + const handleAvatarFile = (file: File) => { if (!file.type.startsWith('image/')) return; const reader = new FileReader(); @@ -646,24 +833,36 @@ function FirstAccount({ try { setLanguage(lang); - await createUser({ + const initialPreferences = showInitialSetup ? { + language: lang, + accentColor: setupAccent, + theme: setupTheme, + wallpaper: setupWallpaper || undefined, + } : { language: lang }; + const createdUser = await createUser({ username: form.username.trim(), displayName: form.displayName.trim(), password: form.password, accountType: form.accountType, avatar: form.avatar ? form.avatar : undefined, - preferences: { language: lang }, + preferences: initialPreferences, recoveryQuestion, recoveryAnswer, autoLogin: false, }); + let updatedCompany: CompanyProfile | undefined; try { const settingRecord = await db.settings.get('company_profile'); const currentCompany: CompanyProfile = (settingRecord?.value as CompanyProfile) || initialCompany || ({} as CompanyProfile); - const updatedCompany: CompanyProfile = { + updatedCompany = { ...currentCompany, language: lang, + ...(showInitialSetup ? { + accent_color: setupAccent, + theme_mode: setupTheme, + desktop_wallpaper_url: setupWallpaper || undefined, + } : {}), ...(form.accountType === 'business' ? { name: businessDetails.companyName.trim() || currentCompany.name || '', ...(!isBusinessSetupSkipped ? { @@ -678,11 +877,44 @@ function FirstAccount({ } : {}) }; await db.settings.put({ key: 'company_profile', value: updatedCompany }); - window.dispatchEvent(new CustomEvent('socdof-company-updated', { detail: updatedCompany })); } catch (compErr) { console.error('Failed to update company profile from onboarding:', compErr); } + if (showInitialSetup) { + const selectedModules = Array.from(new Set([ + ...SETUP_CORE_MODULES, + ...getSetupModulesForAccount(setupApps, form.accountType), + ])); + const selectedDesktopPins = selectedModules; + const selectedTaskbarPins = ['dashboard', 'invoices', 'accounting', 'contacts', 'calendar', 'settings'] + .filter((module): module is ActiveModule => selectedModules.includes(module as ActiveModule)); + const moduleValues: Record = { + odoo_installed_modules: selectedModules, + odoo_pinned_desktop: selectedDesktopPins, + odoo_pinned_taskbar: selectedTaskbarPins, + }; + const scopedPrefix = `socdof.user.${createdUser.id}.`; + try { + Object.entries(moduleValues).forEach(([key, value]) => { + const serialized = JSON.stringify(value); + localStorage.setItem(key, serialized); + localStorage.setItem(scopedPrefix + key, serialized); + localStorage.setItem(scopedPrefix + 'all.' + key, serialized); + }); + } catch (storageErr) { + console.warn('Could not persist all onboarding module preferences:', storageErr); + } + applyAccentColor(setupAccent); + keepSetupPreviewRef.current = true; + const isDark = setupTheme === 'dark' || (setupTheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); + document.documentElement.classList.toggle('dark', isDark); + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; + } + if (updatedCompany) { + window.dispatchEvent(new CustomEvent('socdof-company-updated', { detail: updatedCompany })); + } + clearSession(); onCreated(form.username.trim()); } catch (err) { @@ -701,16 +933,16 @@ function FirstAccount({ }; return ( -

-
+
+
{/* Header with step indicator and language selector */} -
-
+
+
{t('auth.stepAccount', lang)}
-
+
setBusinessDetails({ ...businessDetails, currency: e.target.value })} /> -
+
{['€', '$', 'CHF', '£'].map((curr) => (
-
+
+ {/* Optional first-run appearance and app setup */} +
+
+
+ + + +
+

{t('auth.setupTitle', lang)}

+

{t('auth.setupDescription', lang)}

+
+
+ +
+ + {showInitialSetup ? ( +
+
+
+ {t('auth.setupAccent', lang)} +
+
+ {ACCENT_LIST.map((accent) => ( + + ))} +
+
+ +
+
+ {t('auth.setupTheme', lang)} +
+
+ {([ + { mode: 'light', label: t('auth.setupLight', lang), Icon: Sun }, + { mode: 'dark', label: t('auth.setupDark', lang), Icon: Moon }, + { mode: 'system', label: t('auth.setupSystem', lang), Icon: Monitor }, + ] as const).map(({ mode, label, Icon }) => ( + + ))} +
+
+ +
+
+ {t('auth.setupWallpaper', lang)} +
+ { + void handleSetupWallpaperFile(event.target.files?.[0]); + event.target.value = ''; + }} + /> +
+ {setupWallpaper ? ( + {t('auth.setupWallpaper', + ) : ( +
+ +
+ )} +
+

{t('auth.setupWallpaperHint', lang)}

+
+ + {setupWallpaper && ( + + )} +
+
+
+
+ +
+
+ {t('auth.setupTemplate', lang)} +
+
+ {SETUP_TEMPLATE_KEYS.filter((template) => form.accountType === 'business' || template === 'personal' || template === 'practice').map((template) => { + const templateApps = getSetupModulesForAccount(SETUP_MODULE_TEMPLATES[template], form.accountType); + const isSelected = templateApps.length > 0 && templateApps.every((module) => setupApps.includes(module)) && setupApps.length === templateApps.length; + const TemplateIcon = template === 'personal' ? UserRound : template === 'business' ? BriefcaseBusiness : template === 'practice' ? HeartPulse : ShoppingCart; + return ( + + ); + })} +
+
+

{t('auth.setupApps', lang)}

+ {t('auth.setupAppsHint', lang)} +
+
+ {SETUP_MODULE_OPTIONS.filter((module) => form.accountType === 'business' || !SETUP_BUSINESS_ONLY_MODULES.has(module)).map((module) => { + const isSelected = setupApps.includes(module); + const ModuleIcon = SETUP_MODULE_ICONS[module] || BriefcaseBusiness; + return ( + + ); + })} +
+
+
+ ) : ( +

{t('auth.setupSkippedNotice', lang)}

+ )} +
+ {/* Profile Picture (Optional, gray silhouette default, upload/remove) */}
-
-
+
+
{form.avatar ? ( Avatar ) : ( )}
-
+
-
+
{users.map((user) => )}
{canManage && <>
{avatars.map((avatar) => )}
updateUser(selected.id, { displayName: e.target.value })} />
}
setNewPassword(e.target.value)} />

{text.forcePassword}

-{canManage &&

{text.security}

} -{message &&

{message}

}
{canManage &&
setNewUser({ ...newUser, displayName: e.target.value })} /> setNewUser({ ...newUser, username: e.target.value })} /> setNewUser({ ...newUser, password: e.target.value })} /> setNewUser({ ...newUser, recoveryAnswer: e.target.value })} />
}
; + return

{text.users}

{text.current}: {currentUser.displayName}

{users.map((user) => )}
{canManage && <>
{avatars.map((avatar) => )}
updateUser(selected.id, { displayName: e.target.value })} />
}
setNewPassword(e.target.value)} />

{text.forcePassword}

+{canManage &&

{text.security}

} +{message &&

{message}

}
{canManage &&
setNewUser({ ...newUser, displayName: e.target.value })} /> setNewUser({ ...newUser, username: e.target.value })} /> setNewUser({ ...newUser, password: e.target.value })} /> setNewUser({ ...newUser, recoveryAnswer: e.target.value })} />
}
; } diff --git a/src/components/DocumentationApp.tsx b/src/components/DocumentationApp.tsx index 1f2bf48..26d0e53 100644 --- a/src/components/DocumentationApp.tsx +++ b/src/components/DocumentationApp.tsx @@ -163,6 +163,15 @@ export const DocumentationApp: React.FC = () => { es: 'Accede a todos los módulos comerciales, búsqueda y configuración desde el menú de inicio inferior.' })} +
  • + {getLoc({ de: 'Einstellungen finden: ', en: 'Find a setting: ', fr: 'Trouver un réglage : ', es: 'Buscar un ajuste: ' })} + {getLoc({ + de: 'In Einstellungen führen die Schnellzugriffe zu häufig genutzten Bereichen. Am Desktop wählen Sie Kategorien in der Seitenleiste; in schmalen Fenstern öffnen Sie das gruppierte Bereiche-Menü. Über die Suche finden Sie Einstellungen direkt. Firmendaten ergänzen Sie unter Allgemein & Stammdaten.', + en: 'Settings shortcuts open commonly used sections. On desktop, choose categories in the sidebar; in narrow windows, open the grouped Sections menu. Search finds settings directly. Add company details under General & Master Data.', + fr: 'Les raccourcis des paramètres ouvrent les sections courantes. Sur ordinateur, choisissez une catégorie dans la barre latérale ; dans une fenêtre étroite, ouvrez le menu Sections groupé. La recherche permet de trouver directement un réglage. Ajoutez les informations de l’entreprise dans Général et données principales.', + es: 'Los accesos de Configuración abren las secciones habituales. En escritorio, elige una categoría en la barra lateral; en ventanas estrechas, abre el menú Secciones agrupado. La búsqueda encuentra ajustes directamente. Añade los datos de empresa en General y datos principales.' + })} +
  • @@ -224,6 +233,8 @@ export const DocumentationApp: React.FC = () => {
    • {getLoc({ de: 'Beim ersten Start wird ein lokales Administratorkonto erstellt. Weitere Konten werden durch einen aktiven Administrator angelegt.', en: 'The first setup creates a local administrator. Additional accounts are created by an active administrator.', fr: 'La première configuration crée un administrateur local. Les comptes supplémentaires sont créés par un administrateur actif.', es: 'La primera configuración crea un administrador local. Las cuentas adicionales las crea un administrador activo.' })}
    • +
    • {getLoc({ de: 'Während der ersten Kontoerstellung kann die optionale Arbeitsbereich-Einrichtung übersprungen werden: Akzentfarbe, Hell-/Dunkel-/Systemmodus, lokales Hintergrundbild und App-Vorlagen mit individueller App-Auswahl. Systemmodus ist vorausgewählt; die Auswahl lässt sich später unter Einstellungen anpassen.', en: 'During first-account creation, users can skip the optional workspace setup: accent color, light/dark/system theme, local wallpaper, and app templates with individual app selection. System mode is selected by default; these choices can be changed later in Settings.', fr: 'Lors de la création du premier compte, la configuration facultative de l’espace de travail peut être ignorée : couleur d’accentuation, thème clair/sombre/système, fond d’écran local et modèles d’applications avec sélection individuelle. Le mode système est sélectionné par défaut ; ces choix restent modifiables dans les Paramètres.', es: 'Al crear la primera cuenta se puede omitir la configuración opcional del espacio de trabajo: color de acento, tema claro/oscuro/sistema, fondo local y plantillas de aplicaciones con selección individual. El modo sistema está seleccionado de forma predeterminada y estas opciones se pueden cambiar después en Configuración.' })}
    • +
    • {getLoc({ de: 'Persönlich, Geschäftlich, Praxis und Einzelhandel stellen passende App-Gruppen bereit. Jede App-Kachel zeigt Name, Kurzbeschreibung, Symbol und Auswahlstatus.', en: 'Personal, Business, Practice, and Retail templates provide relevant app groups. Each app card shows its name, short description, icon, and selection state.', fr: 'Les modèles Personnel, Entreprise, Cabinet et Commerce proposent des groupes d’applications adaptés. Chaque carte indique le nom, une courte description, une icône et l’état de sélection.', es: 'Las plantillas Personal, Empresa, Consulta y Comercio ofrecen grupos de aplicaciones adecuados. Cada tarjeta muestra el nombre, una breve descripción, un icono y el estado de selección.' })}
    • {getLoc({ de: 'Persönliche und geschäftliche Kontotypen, Rollen, Aktivierungsstatus, Avatare und persönliche Einstellungen werden pro Benutzer gespeichert.', en: 'Personal and business account types, roles, activation state, avatars, and personal preferences are stored per user.', fr: 'Les types de compte personnel/professionnel, rôles, état d’activation, avatars et préférences sont stockés par utilisateur.', es: 'Los tipos de cuenta personal/empresa, roles, estado, avatares y preferencias se guardan por usuario.' })}
    • {getLoc({ de: 'Die neue Kategorie „Benutzer & Konten“ befindet sich in Einstellungen und ist ausschließlich für aktive Administratoren verfügbar. Dort werden lokale Konten, Rollen, Kontotypen, Avatare, Aktivierung, automatische Sperre und Anmeldesicherheit verwaltet.', en: 'The new “Users & Accounts” category is available in Settings for active administrators only. It manages local accounts, roles, account types, avatars, activation, auto-lock, and sign-in security.', fr: 'La nouvelle catégorie « Utilisateurs et comptes » est disponible dans les paramètres uniquement pour les administrateurs actifs. Elle gère les comptes locaux, rôles, types de compte, avatars, activation, verrouillage automatique et sécurité de connexion.', es: 'La nueva categoría “Usuarios y cuentas” está disponible en Configuración solo para administradores activos. Permite gestionar cuentas locales, roles, tipos, avatares, activación, bloqueo automático y seguridad de inicio de sesión.' })}
    • {getLoc({ de: 'Das SOCDOF-Startmenü enthält über das Ein/Aus-Symbol jetzt Sperren, Benutzer wechseln, Abmelden, Neustart des Arbeitsbereichs und Schließen der App. Die Verwaltung lokaler Benutzer erfolgt zentral in Einstellungen unter „Benutzer & Konten“ und ist nur für aktive Administratoren verfügbar.', en: 'The SOCDOF Start menu power control now provides Lock, Switch user, Sign out, Restart workspace, and Close SOCDOF. Local user administration is centralized in Settings under Users & Accounts and is available only to active administrators.', fr: 'Le contrôle d’alimentation du menu Démarrer de SOCDOF propose désormais Verrouiller, Changer d’utilisateur, Se déconnecter, Redémarrer l’espace de travail et Fermer SOCDOF. La gestion des utilisateurs locaux est centralisée dans Paramètres, sous Utilisateurs et comptes, et réservée aux administrateurs actifs.', es: 'El control de energía del menú Inicio de SOCDOF ofrece ahora Bloquear, Cambiar de usuario, Cerrar sesión, Reiniciar el espacio de trabajo y Cerrar SOCDOF. La administración de usuarios locales está centralizada en Configuración, dentro de Usuarios y cuentas, y solo está disponible para administradores activos.' })}
    • diff --git a/src/components/SettingsModule.tsx b/src/components/SettingsModule.tsx index 5867762..dfb634a 100644 --- a/src/components/SettingsModule.tsx +++ b/src/components/SettingsModule.tsx @@ -211,6 +211,7 @@ export const SettingsModule: React.FC = ({ const [activeSection, setActiveSection] = useState(initialSection || 'home'); const canManageUsers = getCurrentUser()?.role === 'admin' && getCurrentUser()?.active === true; const [searchQuery, setSearchQuery] = useState(''); + const [isMobileSectionMenuOpen, setIsMobileSectionMenuOpen] = useState(false); const rootRef = useRef(null); // Subcategory Navigation in Personalization (Windows 11 Style) @@ -760,14 +761,20 @@ export const SettingsModule: React.FC = ({ // Recent Searches / Quick Links list const recentSearches = useMemo(() => [ - { id: 'personalization', title: t('settings.recent_darkmode_title', activeLang, 'Dark Mode & Design'), category: t('settings.recent_darkmode_cat', activeLang, 'Personalization'), icon: Palette, color: 'text-purple-500 bg-purple-50 dark:bg-purple-950/60' }, - { id: 'shortcuts', title: t('settings.shortcuts', activeLang, 'Tastenkombinationen & Kürzel'), category: t('settings.shortcuts_desc', activeLang, 'Hotkeys & Schnellzugriff'), icon: Keyboard, color: 'text-amber-500 bg-amber-50 dark:bg-amber-950/60' }, - { id: 'language', title: t('settings.recent_language_title', activeLang, 'Language & Region'), category: t('settings.recent_language_cat', activeLang, 'Language & Time'), icon: Globe, color: 'text-sky-500 bg-sky-50 dark:bg-sky-950/60' }, - { id: 'connections', title: t('settings.recent_calendar_title', activeLang, 'Google Calendar & iCal'), category: t('settings.recent_calendar_cat', activeLang, 'Connections'), icon: CalendarIcon, color: 'text-blue-500 bg-blue-50 dark:bg-blue-950/60' }, - { id: 'letterhead', title: t('settings.recent_letterhead_title', activeLang, 'Letterhead & DIN 5008'), category: t('settings.recent_letterhead_cat', activeLang, 'Documents'), icon: FileText, color: 'text-amber-500 bg-amber-50 dark:bg-amber-950/60' }, - { id: 'general', title: t('settings.recent_bank_title', activeLang, 'Bank Details & IBAN'), category: t('settings.recent_bank_cat', activeLang, 'Company'), icon: CreditCard, color: 'text-emerald-500 bg-emerald-50 dark:bg-emerald-950/60' }, - { id: 'storage', title: t('settings.recent_backup_title', activeLang, 'JSON Data Backup'), category: t('settings.recent_backup_cat', activeLang, 'Storage'), icon: HardDrive, color: 'text-indigo-500 bg-indigo-50 dark:bg-indigo-950/60' }, - ], [activeLang, canManageUsers]); + { id: 'personalization', title: t('settings.recent_darkmode_title', activeLang, 'Dark Mode & Design'), category: t('settings.recent_darkmode_cat', activeLang, 'Personalization'), icon: Palette }, + { id: 'shortcuts', title: t('settings.shortcuts', activeLang, 'Tastenkombinationen & Kürzel'), category: t('settings.shortcuts_desc', activeLang, 'Hotkeys & Schnellzugriff'), icon: Keyboard }, + { id: 'language', title: t('settings.recent_language_title', activeLang, 'Language & Region'), category: t('settings.recent_language_cat', activeLang, 'Language & Time'), icon: Globe }, + { id: 'connections', title: t('settings.recent_calendar_title', activeLang, 'Google Calendar & iCal'), category: t('settings.recent_calendar_cat', activeLang, 'Connections'), icon: CalendarIcon }, + { id: 'letterhead', title: t('settings.recent_letterhead_title', activeLang, 'Letterhead & DIN 5008'), category: t('settings.recent_letterhead_cat', activeLang, 'Documents'), icon: FileText }, + { id: 'general', title: t('settings.recent_bank_title', activeLang, 'Bank Details & IBAN'), category: t('settings.recent_bank_cat', activeLang, 'Company'), icon: CreditCard }, + { id: 'storage', title: t('settings.recent_backup_title', activeLang, 'JSON Data Backup'), category: t('settings.recent_backup_cat', activeLang, 'Storage'), icon: HardDrive }, + ], [activeLang]); + + const companyName = profile.name?.trim() || ''; + const companyNamePlaceholder = ['ihr firmenname', 'firmenname', 'unternehmensname', 'your company name', 'company name', 'your company', 'socdof']; + const companyDisplayName = companyName && !companyNamePlaceholder.includes(companyName.toLowerCase()) + ? companyName + : ''; useEffect(() => { loadStorageInfo(); @@ -775,6 +782,19 @@ export const SettingsModule: React.FC = ({ setCustomCalendarEvents(getStoredCalendarEvents()); }, [company]); + useEffect(() => { + if (!isMobileSectionMenuOpen) return; + + const handleOutsidePointerDown = (event: PointerEvent) => { + if (!rootRef.current?.contains(event.target as Node)) { + setIsMobileSectionMenuOpen(false); + } + }; + + document.addEventListener('pointerdown', handleOutsidePointerDown); + return () => document.removeEventListener('pointerdown', handleOutsidePointerDown); + }, [isMobileSectionMenuOpen]); + const loadStorageInfo = async () => { try { const stats = await getDatabaseStorageStats(); @@ -1218,49 +1238,49 @@ export const SettingsModule: React.FC = ({ id: 'overview', label: t('settings.category_overview', activeLang, 'Übersicht'), items: [ - { id: 'home' as SettingsSection, label: t('settings.home', activeLang, 'Startseite'), icon: Settings, desc: 'Schnellzugriff & Personalisierung' } + { id: 'home' as SettingsSection, label: t('settings.home', activeLang, 'Startseite'), icon: Settings, desc: t('settings.section_desc.home', activeLang, 'Quick access to workspace settings.') } ] }, { id: 'system_display', label: t('settings.category_display', activeLang, 'Anzeige & Bildschirme'), items: [ - { id: 'display' as SettingsSection, label: t('settings.display', activeLang, 'Bildschirme & Skalierung'), icon: Monitor, desc: 'Multi-Monitor, Bildschirmlayout, Skalierung & Nachtmodus' } + { id: 'display' as SettingsSection, label: t('settings.display', activeLang, 'Bildschirme & Skalierung'), icon: Monitor, desc: t('settings.section_desc.display', activeLang, 'Manage displays, scaling, and screen layout.') } ] }, { id: 'business', label: t('settings.category_business', activeLang, 'Unternehmen & Workflow'), items: [ - { id: 'general' as SettingsSection, label: t('settings.general', activeLang, 'Allgemein & Stammdaten'), icon: Building2, desc: 'Firmenanschrift, Steuern & Basisdaten' }, - { id: 'payments' as SettingsSection, label: t('settings.payments', activeLang, 'Zahlungsmethoden & Terminals'), icon: CreditCard, desc: t('settings.payments_desc', activeLang, 'Bankverbindung, IBAN/BIC, EC- & Kreditkarten-Terminals') }, - { id: 'letterhead' as SettingsSection, label: t('settings.letterhead', activeLang, 'Briefkopf & DIN 5008'), icon: FileText, desc: 'Logo, Faltmarken & PDF-Layout' }, - { id: 'connections' as SettingsSection, label: t('settings.connections', activeLang, 'Verbindungen & Kalender'), icon: Link2, desc: 'Google Kalender 2-Way Sync & iCal' } + { id: 'general' as SettingsSection, label: t('settings.general', activeLang, 'Allgemein & Stammdaten'), icon: Building2, desc: t('settings.section_desc.general', activeLang, 'Company address, tax details, and master data.') }, + { id: 'payments' as SettingsSection, label: t('settings.payments', activeLang, 'Zahlungsmethoden & Terminals'), icon: CreditCard, desc: t('settings.section_desc.payments', activeLang, 'Bank details, payment methods, and terminals.') }, + { id: 'letterhead' as SettingsSection, label: t('settings.letterhead', activeLang, 'Briefkopf & DIN 5008'), icon: FileText, desc: t('settings.section_desc.letterhead', activeLang, 'Logo, folding marks, and invoice layout.') }, + { id: 'connections' as SettingsSection, label: t('settings.connections', activeLang, 'Verbindungen & Kalender'), icon: Link2, desc: t('settings.section_desc.connections', activeLang, 'Google Calendar sync and iCal tools.') } ] }, { id: 'interface', label: t('settings.category_interface', activeLang, 'System & Personalisierung'), items: [ - { id: 'personalization' as SettingsSection, label: t('settings.personalization', activeLang, 'Personalisierung & Farben'), icon: Palette, desc: 'Darkmode, Akzentfarben & Desktop-Design' }, - { id: 'language' as SettingsSection, label: t('settings.language', activeLang, 'Sprache & Sprachpakete'), icon: Globe, desc: 'Systemsprache, Währung & .JSON Pakete' }, - { id: 'shortcuts' as SettingsSection, label: t('settings.shortcuts', activeLang, 'Tastenkombinationen & Kürzel'), icon: Keyboard, desc: t('settings.shortcuts_desc', activeLang, 'Tastaturkürzel, Hotkeys & Schnellzugriff') }, - { id: 'audio' as SettingsSection, label: t('settings.audio', activeLang, 'Sound & Lautstärke'), icon: Volume2, desc: 'Kassentöne, Effekte & Lautstärkeregler' } + { id: 'personalization' as SettingsSection, label: t('settings.personalization', activeLang, 'Personalisierung & Farben'), icon: Palette, desc: t('settings.section_desc.personalization', activeLang, 'Theme, accent colors, and desktop design.') }, + { id: 'language' as SettingsSection, label: t('settings.language', activeLang, 'Sprache & Sprachpakete'), icon: Globe, desc: t('settings.section_desc.language', activeLang, 'App language, regional formats, and time.') }, + { id: 'shortcuts' as SettingsSection, label: t('settings.shortcuts', activeLang, 'Tastenkombinationen & Kürzel'), icon: Keyboard, desc: t('settings.section_desc.shortcuts', activeLang, 'Keyboard shortcuts, hotkeys, and quick access.') }, + { id: 'audio' as SettingsSection, label: t('settings.audio', activeLang, 'Sound & Lautstärke'), icon: Volume2, desc: t('settings.section_desc.audio', activeLang, 'Sound effects and volume controls.') } ] }, { id: 'accounts', label: t('settings.category_accounts', activeLang, 'Konten & Profile'), items: [ - { id: 'users' as SettingsSection, label: t('settings.user_accounts', activeLang, 'Benutzerkonto & Profilbild'), icon: UserCircle, desc: t('settings.user_accounts_desc', activeLang, 'Profilbild, Kontoinformationen, Rollen & Sicherheit'), badge: canManageUsers ? t('users.admin_badge', activeLang, 'Admin') : undefined } + { id: 'users' as SettingsSection, label: t('settings.user_accounts', activeLang, 'Benutzerkonto & Profilbild'), icon: UserCircle, desc: t('settings.section_desc.users', activeLang, 'Profile, account details, roles, and security.'), badge: canManageUsers ? t('users.admin_badge', activeLang, 'Admin') : undefined } ] }, { id: 'admin', label: t('settings.category_admin', activeLang, 'Wartung & Datensicherheit'), items: [ - { id: 'windows' as SettingsSection, label: t('settings.windows', activeLang, 'Windows Desktop-App'), icon: Monitor, desc: 'Offline-Betrieb, Autostart & EXE' }, - { id: 'storage' as SettingsSection, label: t('settings.storage', activeLang, 'Speicher & Backup'), icon: HardDrive, desc: 'Snapshots, JSON Export & Backup-Ordner' }, + { id: 'windows' as SettingsSection, label: t('settings.windows', activeLang, 'Windows Desktop-App'), icon: Monitor, desc: t('settings.section_desc.windows', activeLang, 'Offline app, startup, and desktop options.') }, + { id: 'storage' as SettingsSection, label: t('settings.storage', activeLang, 'Speicher & Backup'), icon: HardDrive, desc: t('settings.section_desc.storage', activeLang, 'Snapshots, JSON export, and backup location.') }, { id: 'danger' as SettingsSection, label: t('settings.reset_system_title', activeLang, 'System zurücksetzen'), icon: ShieldAlert, danger: true, desc: t('settings.reset_system_desc', activeLang, 'Setzen Sie Ihr gesamtes System zurück und installieren Sie alles neu.') } ] } @@ -1272,16 +1292,22 @@ export const SettingsModule: React.FC = ({ const CurrentIcon = currentNav?.icon || Settings; return ( -
      +
      { + if (event.key === 'Escape') setIsMobileSectionMenuOpen(false); + }} + className="relative h-full w-full flex flex-col overflow-hidden animate-fade-in text-slate-900 dark:text-slate-100" + > {/* 1. Sleek, Space-Efficient Top Toolbar */} -
      -
      -
      +
      +
      +
      {/* Left: Current View Indicator */}
      -
      +
      @@ -1299,15 +1325,15 @@ export const SettingsModule: React.FC = ({
      {/* Right: Fixed Position Search Box & Quick Save Action */} -
      -
      +
      +
      setSearchQuery(e.target.value)} - className="w-full pl-8 pr-7 py-1.5 text-xs bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-xl text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 shadow-xs transition" + className="w-full pl-8 pr-7 py-1.5 text-xs bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-xl text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)] shadow-xs transition" /> {searchQuery && ( ))}
      @@ -1384,47 +1410,80 @@ export const SettingsModule: React.FC = ({ )}
      - {/* Mobile / Small Screen Compact Horizontal Category Pills (prevents huge vertical stack) */} -
      - {navItems.map((item) => { - const Icon = item.icon; - const isActive = activeSection === item.id; - return ( - - ); - })} + {/* The themed section menu avoids the low-contrast native select popup on compact windows. */} +
      +
      + + +
      + {categoryGroups.map((group) => ( +
      +
      + {group.label} +
      +
      + {group.items.map((item) => { + const Icon = item.icon; + const isActive = activeSection === item.id; + return ( + + ); + })} +
      +
      + ))} +
      +
      {/* 2. Main Responsive Two-Column Layout (Windows Settings Style Sidebar + Content) */} -
      -
      +
      +
      {/* Left Sidebar Navigation (Desktop / Tablet view: side by side, independently scrollable) */}
      @@ -1574,21 +1633,32 @@ export const SettingsModule: React.FC = ({ {activeSection === 'home' && (
      - {/* Windows 11 Profile Banner */} -
      -
      -
      -
      - {profile.name ? profile.name.slice(0, 2).toUpperCase() : 'SO'} + {/* Workspace overview */} +
      +
      +
      +
      + {companyDisplayName ? companyDisplayName.slice(0, 2).toUpperCase() : }
      -
      -

      {profile.name || t('settings.your_company', activeLang, 'Your Company')}

      -

      {profile.email || t('settings.no_email_hint', activeLang, 'No email configured')} • {profile.city || t('settings.default_country', activeLang, 'Germany')}

      -
      - +
      +

      + {t('settings.category_overview', activeLang, 'Übersicht & Schnelleinstieg')} +

      +

      + {companyDisplayName || t('settings.workspace_title', activeLang, 'Your workspace')} +

      +

      + {[profile.email, profile.city].filter(Boolean).join(' · ') || t('settings.workspace_local_ready', activeLang, 'Local workspace · Add your company details anytime.')} +

      +
      + + {t('settings.offline_free_badge', activeLang, '100% Offline & Free')} - + {t('settings.din5008_ready', activeLang, 'DIN 5008 Ready')}
      @@ -1596,28 +1666,32 @@ export const SettingsModule: React.FC = ({
      - {/* Zuletzt gesucht & Empfohlene Einstellungen (Windows Inspired) */} -
      + {/* Quick access to recommended settings */} +

      - - {t('settings.recent_quick_start', activeLang, 'Recent Searches & Quick Access')} + + {t('settings.recent_quick_start', activeLang, 'Quick access')}

      - {t('settings.recent_quick_start_desc', activeLang, 'Frequently used settings and personalization options at a glance:')} + {t('settings.recent_quick_start_desc', activeLang, 'Jump directly to commonly used settings.')}

      -
      - {recentSearches.map((tile) => { +
      + {recentSearches.map((tile, recentIndex) => { const Icon = tile.icon; return ( ); })} @@ -1647,15 +1726,16 @@ export const SettingsModule: React.FC = ({
      {/* Quick Preferences Toggles */} -
      +
      {/* 1. Dark Mode Quick Switch */}
      { sounds.playClick(); onToggleTheme(); }} - className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-indigo-300 dark:hover:border-indigo-800 rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none" - role="button" + className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-[var(--accent-border-subtle)] rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-ring)]" + role="switch" + aria-checked={isDark} tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { @@ -1666,9 +1746,7 @@ export const SettingsModule: React.FC = ({ }} >
      -
      +
      {isDark ? : }
      @@ -1684,8 +1762,9 @@ export const SettingsModule: React.FC = ({ {/* Elegant Animated Toggle Switch */}
      = ({ sounds.playClick(); onToggleSound(); }} - className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-emerald-300 dark:hover:border-emerald-800 rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none" - role="button" + className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-[var(--accent-companion-border)] rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-ring)]" + role="switch" + aria-checked={!isMuted} tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { @@ -1713,9 +1793,7 @@ export const SettingsModule: React.FC = ({ }} >
      -
      +
      {!isMuted ? : }
      @@ -1731,8 +1809,9 @@ export const SettingsModule: React.FC = ({ {/* Elegant Animated Toggle Switch */}
      = ({ sounds.playClick(); handleSelectSection('connections'); }} - className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-blue-300 dark:hover:border-blue-800 rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none" + className="p-3.5 bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-[var(--accent-border-subtle)] rounded-2xl flex items-center justify-between cursor-pointer transition shadow-xs group select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-ring)]" role="button" tabIndex={0} onKeyDown={(e) => { @@ -1760,7 +1839,7 @@ export const SettingsModule: React.FC = ({ }} >
      -
      +
      @@ -1772,7 +1851,7 @@ export const SettingsModule: React.FC = ({
      -
      +
      {t('settings.quick_open', activeLang, 'Öffnen')}
      @@ -1784,7 +1863,7 @@ export const SettingsModule: React.FC = ({

      - + {t('settings.category_overview', activeLang, 'Einstellungsbereiche & Module')}

      @@ -1793,7 +1872,7 @@ export const SettingsModule: React.FC = ({

      -
      +
      {categoryGroups.filter(g => g.id !== 'overview').map((group) => (
      = ({ >
      - + {group.label}
      @@ -1818,14 +1897,14 @@ export const SettingsModule: React.FC = ({ className={`w-full p-2.5 rounded-2xl text-left transition flex items-start justify-between group ${ item.danger ? 'hover:bg-rose-50/80 dark:hover:bg-rose-950/30' - : 'hover:bg-indigo-50/80 dark:hover:bg-indigo-950/30' + : 'hover:bg-[var(--accent-light)]' }`} >
      @@ -1833,7 +1912,7 @@ export const SettingsModule: React.FC = ({
      {item.label}
      @@ -1842,7 +1921,7 @@ export const SettingsModule: React.FC = ({
      - + ); })} @@ -5770,7 +5849,7 @@ export const SettingsModule: React.FC = ({ value={shortcutSearch} onChange={(e) => setShortcutSearch(e.target.value)} placeholder={t('settings.shortcuts_search_placeholder', activeLang, 'Tastenkürzel suchen...')} - className="w-full pl-8 pr-7 py-1.5 text-xs bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-xl text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 shadow-xs transition" + className="w-full pl-8 pr-7 py-1.5 text-xs bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-xl text-slate-900 dark:text-white placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)] shadow-xs transition" /> {shortcutSearch && (