Tento soubor je průběžný uživatelský a provozní manuál projektu.
- Popsat, jak projekt spustit, používat, nasadit a spravovat.
- Udržovat informace aktuální při každé významné změně.
- Přehled projektu
- Lokální spuštění
- Konfigurace prostředí
- Build a nasazení
- Provoz a monitoring
- Řešení problémů (Troubleshooting)
- Přehled architektury je v
ARCHITECTURE.md. - Detail veřejného i self-service rezervačního toku je v
BOOKING_FLOW.md. - Stručný produkční deploy přehled pro Proxmox/LXC je v
DEPLOYMENT.md. - Stručný runtime přehled proměnných a prostředí je v
ENVIRONMENT.md. - Nejčastější provozní potíže a jejich první diagnostika jsou v
TROUBLESHOOTING.md.
- Při každé funkční změně aktualizuj relevantní sekce.
- Při změně nasazení vždy aktualizuj sekci Build a nasazení.
- Pokud přibude nová chyba a její fix, doplň ji do Troubleshooting.
OWNER spravuje šablony v /admin/vouchery/sablony. Draft lze nahrát a upravit v milimetrech; publikovanou šablonu nelze změnit, pro další grafickou verzi vytvoř novou verzi. Před prvním použitím po nasazení spusť npm run voucher:templates:bootstrap. Zálohuj vždy databázi i MEDIA_STORAGE_ROOT, protože obsahuje privátní PDF mastery. Neaktivní šablony se nesmí používat pro nové vouchery, ale zůstávají potřebné pro historický tisk a e-mail.
- Doporučený produkční rollout script je
deploy/release.sh. - Spouštěj z rootu repozitáře:
cd /var/www/ppstudio./deploy/release.sh
- Skript před releasem ověří, že na serveru existují units
ppstudio-web.serviceappstudio-email-worker.service; pokud chybí, skončí s návodem nasudo /var/www/ppstudio/deploy/deploy.sh. - Skript také hlídá, že stejné procesy neběží ještě přes legacy PM2; při konfliktu vypíše převod na čistý systemd provoz (
pm2 delete ...,pm2 save --force,systemctl disable --now pm2-root.service). - Při každém releasu skript navíc synchronizuje aktuální unit soubory z
deploy/systemd/do/etc/systemd/system/a provedesystemctl daemon-reload, takže změny v service definicích nečekají na ručnídeploy/deploy.sh. - Skript před buildem načte
.envjako dotenv soubor, takže fungují i neuzavřené hodnoty s mezerami typuNEXT_PUBLIC_APP_NAME=PP Studio; zároveň vynutí přítomnost validníhoNEXT_SERVER_ACTIONS_ENCRYPTION_KEY, pro aktuální release automaticky exportujeNEXT_DEPLOYMENT_ID,DEPLOYMENT_VERSIONiGIT_HASHz aktuálního git commitu a stejnou trojici zapíše do runtime souboru.release-env, který čte systemd služba webu. - Protože produkční
.envběžně nastavujeNODE_ENV=production, skript používánpm ci --include=dev, aby se nainstalovaly i build-time nástroje zdevDependenciesjakoeslint,typescriptaprisma. - Release build neběží nad živým runtime. Po úspěšném buildu vznikne úplný adresář
/var/www/ppstudio/releases/<commit>-<čas>a systemd vždy spouští/var/www/ppstudio/current. Krátké atomické přepnutí symlinku proto přepne zdrojové soubory,.next,node_modulesi workertsxruntime společně; předchozí release zůstává zachovaný přes symlinkpreviousaž do úspěšného health a smoke testu. - Po úspěšném release se adresář
releases/automaticky uklidí: chráněné jsou pouze cílecurrentaprevious; další release se ve výchozím nastavení neuchovávají. Limit upravíš přes./deploy/release.sh --keep-releases 14; úklid nikdy neběží při selhání releasu. package.jsonzároveň drží npm 11allowScriptswhitelist pro balíčky s install hooky (prisma,@prisma/engines,sharp,esbuild,unrs-resolver), takže release nevypisuje opakovanénpm warn allow-scriptsa při upgradu těchto balíčků je potřeba whitelist znovu vědomě potvrdit.- Skript před vytvořením čistého git-archive workspace odmítne lokální migrační adresáře bez
migration.sql(ochrana před Prisma P3015). Bez zápisu do DB provedenpm ci, generate, kontrolu historie,prisma validate, lint, samostatnýtypechecka build; potom zastaví web i worker, ověří jejich neaktivní stav, spustíprisma migrate deploya z nového release povinný idempotentnínpm run voucher:templates:bootstrappřed aktivací. Při selhání migrace, bootstrapu nebo startu zůstávají služby fail-closed zastavené, dokud není ručně potvrzen bezpečný recovery postup. - Kontrola historie může uvést rollbacknuté záznamy
20260419103000_service_public_bookability,20260419140000_site_settings_singletona20260428133959_voucher_pdf_logo_settings. Jsou následované úspěšným záznamem stejné migrace, takže přiMigration history check: OKnepředstavují blocker releasu ani se nesmějí ručně mazat z_prisma_migrations. next.config.tsexplicitně nastavujeturbopack.rootna adresář právě běžícího checkoutu/release. Staging release tak může mít vlastnípackage-lock.jsonbez falešného workspace warningu přinext build.- Pokud release neprojde health/smoke krokem, skript vypíše, zda selhala liveness/readiness kontrola nebo homepage
/, vždy s HTTP statusem. Teprve podle této informace čtijournalctl -u ppstudio-web.service -n 200 --no-pager. - Po restartu služeb helper nejdřív tiše čeká na otevření webového endpointu (výchozích 20 pokusů po 0,25 s). Tím se očekávaný krátký start Next.js nezamění za incident; až vyčerpání readiness pokusů spouští rollback. Volitelně jej upravíš přes
PPSTUDIO_WEB_READY_RETRIESaPPSTUDIO_WEB_READY_RETRY_SECONDS. - Detailní release checklist a QA body zůstávají v
docs/DEPLOYMENT.md.
npm testspouští celý Node test runner nad quoted globemsrc/**/*.test.{ts,tsx}; nejde už jen o shell-expanded podmnožinu jednoho souboru.npm run typecheckje samostatná rychlá kontrola TypeScript kontraktů bez buildu; pouštěj ji před většími refaktory a vždy při změnách sdílených typů, route kontraktů nebo Prisma read modelů.npm run test:coveragegeneruje report docoverage/a zaměřuje se na business logiku vbooking,admin,vouchersalib/email.- Při refaktoringu velkých admin obrazovek drž odděleně route/data loading (
src/features/admin/lib/**), čisté helpery (src/features/admin/components/*-helpers.ts) a samotnou React kompozici. Tím jde měnit strukturu bez regresí v route kontraktu nebo hydrataci. - Admin planner
Volná oknastále edituje půlhodinová okna, ale vizuálně umí rozlišit i cleanup/volno uvnitř jedné buňky po 15 minutách: žlutá značí úklid, zelená volno a červená navazující službu. - Seznam
Volná oknase skládá až nad merged volnými segmenty, takže sousední publikované sloty bez mezery se v inspektoru dne zobrazí jako jedno souvislé půlhodinově editovatelné okno. - Výstupy jsou připravené pro lokální čtení i CI:
- HTML report v
coverage/index.html - LCOV data v
coverage/lcov.info - strojově čitelný souhrn v
coverage/coverage-summary.json
- HTML report v
- Coverage běh je záměrně bez
RUN_DB_INTEGRATION_TESTS=1, takže reprezentuje hlavně unit/business vrstvu; databázové integrační testy dál ověřuje samostatnýnpm test. - GitHub Actions teď jako baseline pouští osm samostatných CI jobů:
linttypechecktestbuilde2ee2e chromium shard 2e2e mobilee2e mobile shard 2
- Coverage je součástí jobu
testpřesnpm run test:cia ukládá se jako artefakt; nevzniká pro něj samostatný GitHub check. - Joby
testae2ezůstávají povinnou CI branou před merge/releasem. Release skript je záměrně znovu nespouští nad produkčním.env: oba používají databázi a E2E provádějí zapisující scénáře. - Z hlavního CI běhu se ukládají artifacty
coverage-reportaplaywright-report, takže při pádu PR není potřeba vše znovu reprodukovat jen kvůli detailní diagnostice. - Samostatné GitHub workflow navíc řeší
CodeQL,Dependency Review, schedulednpm audit --audit-level=higha týdenní Dependabot update PR pronpmi GitHub Actions. - GitHub Actions baseline je průběžně držena na novější major řadě bez Node 20 warningů v runneru:
actions/checkout@v7,actions/setup-node@v7,actions/upload-artifact@v7aactions/dependency-review-action@v5. - Samotné workflow soubory v repu nestačí k úplnému enforcementu. Po změně CI vždy ručně zkontroluj nastavení repozitáře na GitHubu:
- branch protection / rulesets
- required status checks pro jednotlivé GitHub job names
lint,typecheck,test,build,e2e,e2e chromium shard 2,e2e mobile,e2e mobile shard 2;coveragenení samostatný check - zapnutý Dependabot alerts a security overview
- Aktuální testovací batch (2026-05-19) doplnil unit testy pro
src/features/admin/actions/*action-state.tsa early-fail validace vsrc/features/booking/lib/booking-public/engine.ts(invalid startsAt,invalid phone), aby se zlepšilo pokrytí nejnižších oblastí. - Navazující batch (2026-05-19) přidal validační unit testy pro server actions v
src/features/admin/actions/actions-validation.test.ts(invalid form payloady proclient-actions,service-actions,booking-actions,settings-actions) a zvýšil coverage především vadmin/actions. - Další rozšíření stejného validačního test souboru přidalo i pokrytí pro
service-category-actions(createServiceCategoryAction,updateServiceCategoryAction) nad chybnými payloady, aby se dál zvedlo coverage v admin action vrstvě bez DB flaky závislostí. - Nová helper vrstva pro admin detail rezervace a weekly planner má vlastní úzké unit testy. Při dalších změnách nejdřív přidej nebo uprav helper test, teprve potom přepisuj JSX nebo server route factory.
- Booking regresní sada nově obsahuje i
src/features/booking/lib/booking-local-time.test.ts(Prague wall-clock převod) asrc/features/booking/lib/booking-manual.integration.test.ts(ruční rezervace přes slot vs. explicitní manual override), takže při změnách admin booking flow pouštěj vedle běžných booking testů i tyto soubory. - Pro širší booking regresi nově počítej i s
src/features/booking/lib/booking-public-voucher.integration.test.ts(vytvoření veřejné rezervace, obsazený slot, snapshot služby/ceny/délky) asrc/features/booking/lib/booking-email-worker.integration.test.ts(24h reminder scheduler + e-mail worker delivery).
- Připrav Node.js 24 LTS, npm 10+ a PostgreSQL 15+.
- Naklonuj repozitář a v rootu vytvoř
.envz.env.example. - Nastav aspoň
DATABASE_URL,SHADOW_DATABASE_URL,ADMIN_SESSION_SECRET,NEXT_PUBLIC_APP_URLa lokálníMEDIA_STORAGE_ROOT. - Pro lokální vývoj preferuj
EMAIL_DELIVERY_MODE=log, aby se neposílaly reálné e-maily. - Spusť
npm install. - Spusť
npm run db:generate. - Spusť
npm run db:migrate. - Spusť
npm run deva otevřihttp://localhost:3000. - Pro první admin přihlášení vytvoř DB OWNERa offline příkazem
npm run admin:recover-owner -- --email owner@example.com --name 'Jméno' --confirm < heslo.txt.
Poznámka k runtime:
- Repo nově cílí na
Node 24 LTS. - Lokálně použij
nvm usepodle.nvmrcnebo jiný ekvivalentní version manager. - V CI i produkci drž stejnou major verzi Node, aby
npm ci, build a nativní balíčky (sharp, Prisma) běžely nad stejným ABI.
- Deaktivace administrátorského účtu je bezpečnostní zásah: v jednom databázovém commitu zneplatní i všechny dosud nepoužité pozvánky. Starý odkaz
/admin/pozvanka/[token]proto musí skončit chybou a nikdy nesmí účet znovu aktivovat; pro návrat přístupu účet znovu aktivuje výhradně owner a podle potřeby odešle novou pozvánku. - Pokud se v dev browser konzoli po restartu
next devobjevíChunkLoadError: Failed to load chunk /_next/static/chunks/..., aplikace se teď sama tvrdě obnoví už z inlinebeforeInteractiveguardu v root layoutu, tedy ještě před hydrací Reactu. Guard nově používá krátké časové okno se dvěma pokusy a dočasný cache-busting query parametr, takže se nezasekne na trvalémsessionStoragelocku po prvním nepovedeném reloadu. Když chyba zůstane i potom, spusťnpm run dev:clean; jde obvykle o rozjetý Turbopack/HMR cache stav, ne o chybu business logiky stránky. - Pokud v admin formuláři
Vytvořit voucherna Windows/Chrome po přepnutí naPoukaz na službunevidíš názvy služeb, nehledej starou browser cache. Šlo o browser-specific rendering nativního dropdownu; aktuální picker používá vlastní seznam služeb a problém už nemá být reprodukovatelný. - Pokud DB test nebo storno rezervace spadne na
AvailabilitySlot_active_time_window_excl, zkontroluj pořadí merge operací vbooking-slot-compaction.ts. Sousední slot musí být při compaction nejdřív archivovaný a teprve potom se může rozšířit cílový interval; jinak PostgreSQL zachytí dočasný překryv dvou aktivních slotů. - Kapacita slotu je pevně
1, protože studio obsluhuje jedna osoba. Pokud migrace hlásí slot s jinou kapacitou, před opakováním rolloutů jej ručně prověř a oprav; nesnižuj hodnotu naslepo, dokud neověříš případné souběžné rezervace. - Pokud admin hlásí, že ruční rezervace „ukazuje jiný čas než se pak uloží“, ověř, že client preview stále používá Prague helper
resolvePragueLocalDateTime(...)a ne lokálnínew Date(...)z browser timezone. Platí to pro drawerPřidat rezervacii proPřesunout termín. - Pokud se při výběru konkrétního slotu v adminu vrací chyba dostupnosti po změně služby nebo po delší prodlevě, je to očekávané: slot-mode už nesmí tiše vytvořit interní výjimku. Pro záměrnou interní výjimku použij režim ručního data/času.
- Pokud ruční booking pro vybranou existující klientku odchází bez e-mailu, backend má zachovat původní
Client.email; prázdné pole ve formuláři není signál pro smazání kontaktu. Opačné chování ber jako regresi. - Pokud admin login po odeslání formuláře vrací
error=origin_check_failed, zkontrolujNEXT_PUBLIC_APP_URL, reverzní proxy hlavičkyHost/X-Forwarded-Hosta že formulář opravdu běží na stejném originu jako/admin/prihlaseni.
NODE_ENV=development
NEXT_PUBLIC_APP_NAME=PP Studio
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_SITE_DOMAIN=ppstudio.cz
VOUCHER_PUBLIC_DOMAIN=ppstudio.cz
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=replace-with-openssl-rand-base64-32
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ppstudio?schema=public"
SHADOW_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ppstudio_shadow?schema=public"
ADMIN_SESSION_SECRET=replace-with-long-random-secret-at-least-32-chars
EMAIL_DELIVERY_MODE=log
EMAIL_TRANSPORT=smtp
MEDIA_STORAGE_ROOT=/var/www/ppstudio-uploadsNEXT_PUBLIC_APP_URLje runtime URL aplikace pro redirecty a e-mailové odkazy.NEXT_PUBLIC_SITE_DOMAINaVOUCHER_PUBLIC_DOMAINslouží jako povolené veřejné domény pro kontrolu důvěryhodného hostu u requestů. Neřídí texty ani grafiku voucher PDF.NEXT_PUBLIC_SITE_URLje doporučená kanonická veřejná URL pro SEO metadata a JSON-LD (při chybějící hodnotě fallback naNEXT_PUBLIC_APP_URL).NEXT_PUBLIC_GOOGLE_ADS_ENABLEDaNEXT_PUBLIC_GOOGLE_ADS_IDvolitelně zapínají veřejný Google Ads tag (gtag.js, typickyAW-*).NEXT_SERVER_ACTIONS_ENCRYPTION_KEYje povinný stabilní klíč pro Next.js Server Actions; v produkci musí zůstat stejný mezi instancemi stejného buildu.DATABASE_URLje hlavní aplikační databáze.SHADOW_DATABASE_URLpoužívá Prisma přimigrate dev.ADMIN_SESSION_SECRETpodepisuje admin session cookie a musí být unikátní pro prostředí.- Admin login/logout route navíc kontrolují stejný
Origina důvěryhodný host protiNEXT_PUBLIC_APP_URL; při nesouladu login skončíerror=origin_check_faileda logout vrátí403. - Recovery admin přístupu nepoužívá env přihlášení; použij auditovatelný offline příkaz
admin:recover-owner. EMAIL_DELIVERY_MODE=logje bezpečný lokální režim bez SMTP odesílání.EMAIL_TRANSPORTurčuje background transport (smtpneboresend).MEDIA_STORAGE_ROOTje zapisovatelná absolutní cesta mimo repo pro nahraná média.- Admin upload médií běží přes Next.js Server Actions. Aplikační limit obrázku je
8 MB, alenext.config.tsdrží request limit10mb, aby multipart overhead nesrazil legitimní upload ještě před serverovou validací. - Produkční nasazení s veřejnými formuláři nad Next.js Server Actions musí používat jednotný
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYpro všechny instance stejného buildu a zároveň posílat konzistentníNEXT_DEPLOYMENT_IDneboDEPLOYMENT_VERSION/GIT_HASH, ze kterýchnext.config.tsskládádeploymentId. Jinak po deployi hrozíFailed to find Server Actionu uživatelky se starším otevřeným tabem. - Při doporučeném rolloutu přes
deploy/release.shseNEXT_DEPLOYMENT_IDdo.envběžně nepíše ručně; skript ho pro každý build automaticky nastaví na aktuální git commit a vedle build env ho zapíše i do.release-envpro runtime logy a incident diagnostiku. V.envtedy drž hlavně stabilníNEXT_SERVER_ACTIONS_ENCRYPTION_KEY. - Next.js runtime teď zapisuje při startu i při zachycené request chybě strukturované logy s prefixy
ppstudio.next.registerappstudio.next.request-error. UFailed to find Server Actionlog obsahuje bezpečný fingerprintNEXT_SERVER_ACTIONS_ENCRYPTION_KEY, aktivnídeploymentId, příchozíx-deployment-id, route context, sanitizovanou path bez raw tokenů a nově i shrnutínext-actionheaderu (length, fingerprint, krátký sample,looksMalformed) pro odlišení stale klienta od scan/probingu.
Detailní seznam všech env proměnných je v docs/ENVIRONMENT.md.
Praktický přehled hlavních HTTP endpointů je v docs/API.md.
- Externí monitoring má pravidelně volat
GET /api/health; při503nebo timeoutu ber stav jako incident. GET /api/health/liveověřuje pouze běh webového procesu;GET /api/healthpřidává jediný DB readiness dotaz a nevrací interní metadata.- Detail fronty, workeru a release identity načti po owner přihlášení z
GET /api/health/diagnostics; konkrétní příčinu chyb hledej vjournalctl -u ppstudio-web.service. - Při nedostupné DB endpoint vrací pouze stabilní
error.code=DATABASE_UNAVAILABLE, nikdy raw detail ovladače nebo připojení. Owner alert je best-effort, neblokuje odpověď a v jednom procesu se pro tento stav odešle nejvýše jednou za 10 minut. - Vedle webu sleduj i běh
ppstudio-web.serviceappstudio-email-worker.service. - Pravidelně kontroluj, že e-mailová fronta nemá rostoucí
failed,retryingnebostalezáznamy. - Po každém releasu proveď minimální smoke test: homepage, admin login a vytvoření testovací rezervace.
- Pokud používáš Matomo reporting pro dashboard, po změně konfigurace nebo incidentu spusť
npm run analytics:check.
- Projekt používá Semantic Versioning
MAJOR.MINOR.PATCHvpackage.json. - Aktuální release je
3.28.0; stabilní řada projektu začala verzí1.0.0. PATCH(0.1.0 -> 0.1.1) zvyšuj při opravách chyb, interním refaktoru bez změny chování a technických úpravách bez dopadu na veřejné rozhraní.MINOR(0.1.0 -> 0.2.0) zvyšuj při přidání nové funkce nebo rozšíření existující funkcionality zpětně kompatibilním způsobem.MAJOR(0.1.0 -> 1.0.0nebo1.x.y -> 2.0.0) zvyšuj při nekompatibilní změně API, datového kontraktu, routingu nebo provozního chování, které vyžaduje zásah uživatele/operátora.- Každé zvýšení verze musí mít odpovídající záznam v
CHANGELOG.mdpod sekcíUnreleased. - Před release se verze v
package.jsonapackage-lock.jsonmění atomicky jedním commitem společně s finální podobou release poznámek.
- Projekt běží na Next.js 16 App Routeru se strukturou oddělenou na public web, booking a admin.
- Veřejný shell (
SiteShell) inicializuje volitelný Matomo tracking přesNEXT_PUBLIC_MATOMO_ENABLED,NEXT_PUBLIC_MATOMO_URLaNEXT_PUBLIC_MATOMO_SITE_ID; admin route group tracking komponentu nepoužívá a při přítomné admin session cookieppstudio-admin-sessionMatomo nenačítá ani na veřejných stránkách. - Veřejný shell (
SiteShell) umí volitelně inicializovat i Microsoft Clarity přesNEXT_PUBLIC_CLARITY_ENABLEDaNEXT_PUBLIC_CLARITY_PROJECT_ID; Clarity běží jen na veřejných/booking stránkách, nepouští se pro přihlášený admin session kontext a neinicializuje se na tokenových self-service routách. - Veřejný shell (
SiteShell) umí volitelně inicializovat i Google Ads tag přesNEXT_PUBLIC_GOOGLE_ADS_ENABLEDaNEXT_PUBLIC_GOOGLE_ADS_ID; tag běží jen na veřejných/booking stránkách, nepouští se pro přihlášený admin session kontext, neinicializuje se na tokenových self-service routách a při klientské navigaci v App Routeru dopošle další pageview přesgtag('config', ...). - Veřejný shell (
SiteShell) umí volitelně inicializovat i Meta Pixel přesNEXT_PUBLIC_META_PIXEL_ENABLEDaNEXT_PUBLIC_META_PIXEL_ID; Pixel běží jen na veřejných/booking stránkách, nepouští se pro přihlášený admin session kontext a neinicializuje se na tokenových self-service routách. - Meta Pixel nad rámec
PageViewsleduje i klíčové neosobní funnel kroky:ViewContentna detailu služby, customBookingServiceSelectedpři výběru služby,BookingDateSelected/BookingTimeSelected,InitiateCheckoutaž po výběru termínu,BookingContactStartedpři první interakci s kontaktem a po úspěchuSchedule.Schedulenení platba a bez jednoznačné finální ceny neobsahuje hodnotu. - Web Vitals tracking má vlastní klientský feature flag
NEXT_PUBLIC_WEB_VITALS_ENABLED(defaulttrue), takže měření lze vypnout nezávisle na pageview/event trackingu v Matomo. - Matomo bootstrap na veřejném webu běží přes inline
next/scriptsafterInteractive, aby tokenové self-service stránky měly bezpečnou_paqqueue připravenou spolehlivě hned po interaktivitě.MatomoTrackeri helperensureMatomoTrackingPathsdílí stejný bootstrap stav, takže ani při pomalejší hydrataci nebo CI se neztratí první bezpečnésetCustomUrlpro tokenové route a raw tokenové URL se dál nikdy neposílají. - Booking-only layout styly pro landscape header a sticky CTA jsou načítané jen v route group
(booking)přessrc/app/(booking)/booking-layout.css; homepage je nedostává z root globálního CSS. - Homepage hero preferuje jako LCP kandidát logo: je preloadované přes
next/image, zatímco portrait běží bez priority, aby první vykreslení nebylo bržděné konkurenčním načítáním. - Veřejné kontaktní e-maily se uživatelkám zobrazují v běžném čitelném tvaru s
@, ale veřejný web dál nesází surovémailto:přímo do SSR HTML; kontaktní odkazy se skládají až v klientu přesObfuscatedEmailLink. - Matomo měří pageview veřejných stránek a booking flow včetně klientských App Router navigací, ale neposílá pageview pro
/admin,/api, Next internals ani tokenové self-service route/rezervace/sprava/*,/rezervace/storno/*,/rezervace/akce/*. - Veřejný web obsahuje statický ověřovací soubor Seznam Webmasteru v
public/seznam-wmt-cjKzOuv71FG0TOfkMT7WBqHwAXFWhvum.txt; po nasazení musí být dostupný nahttps://ppstudio.cz/seznam-wmt-cjKzOuv71FG0TOfkMT7WBqHwAXFWhvum.txt. - Rezervační flow posílá pouze neosobní eventy
Rezervace / Služba vybrána,Datum vybráno,Čas vybrán,Kontakt zahájen,Odeslána rezervace,Formulář chyba,Termín konflikt při odeslánía po úspěchuVytvořena; při prázdném stavu katalogu navíc umí zaznamenatRezervace / Bez služebneboRezervace / Bez termínů. První krok dashboard funnelu (viewed) se už nebere z custom eventu, ale ze samotných pageview/rezervacev Matomu. Self-service změna termínu posílá bezpečnéRezervace / Datum vybránoaRezervace / Čas vybrán. Předvyplnění služby přes URL (/rezervace?service=...) se zapisuje samostatně jakoRezervace / Služba předvyplněna, ale už neduplikuje funnel eventSlužba vybrána; ten zůstává jen pro skutečné ruční zvolení služby v UI.Datum vybránoje v booking flow i self-service deduplikované na první výběr konkrétního dne, takže klik na následný čas v tom samém dni už neposílá druhý stejný date event.Kontakt zahájense odešle až při první interakci s kontaktním polem,Formulář chybaagreguje klientskou chybu kontaktního kroku nebo serverový submit error aTermín konflikt při odesláníse spouští jen pro konflikt/invalidaci vybraného slotu. Jméno, e-mail, telefon, poznámka ani tokeny se do analytics neposílají. - Self-service správa rezervace navíc bezpečně měří
Rezervace / Změna termínu otevřena,Rezervace / Změna termínu odeslána,Rezervace / Storno odeslánoaRezervace / Storno dokončeno; privacy guard v helperu blokuje jen PII a raw tokenové cesty, ne samotné slovostorno. - Playwright smoke pokrývá i veřejné storno přes token: tokenová stránka nesmí poslat pageview s raw URL, ale po kliknutí na potvrzení musí projít bezpečné Matomo eventy
Storno odeslánoaStorno dokončenobez úniku tokenu do_paq. - Předvyplněný vstup na
/rezervace?service=...z veřejného ceníku nebo detailu služby nově posílá i samostatný eventRezervace / Služba předvyplněna, takže v Matomu lze odlišit intent-rich vstup z jiné stránky od ručního výběru služby až uvnitř booking flow. - Volitelný telefon v rezervaci se zadává přirozeně (
777 123 456,+420 777 123 456,00420 777 123 456), ale doClient.phonea booking snapshotu se ukládá normalizovaně bez mezer v mezinárodním tvaru (+420777123456). Text, HTML a nejasná krátká čísla server odmítá hláškou s příklady formátu. - Poznámka od klientky se ukládá k rezervaci a v provozním e-mailu o nové čekající rezervaci se ukáže majiteli/salonu jako kontext pro schválení. Klientské booking e-maily ji dál neobsahují.
- Když klientka přes self-service web přesune termín rezervace, na
notificationAdminEmailnově odchází i provozní notifikaceRezervace přesunuta klientkous původním/novým termínem a přímým odkazem na detail rezervace v adminu. - V Matomo lze ručně nastavit Goal pro vlastní Matomo přehledy: název
Rezervace vytvořena, triggercustom event, categoryRezervace, actionVytvořena. Admin widget ale hlavní počet rezervací bere přímo z eventuRezervace / Vytvořena, aby KPI a funnel držely stejnou definici. - Server-side dashboard analytics používají
MATOMO_URL,MATOMO_SITE_IDa tajnýMATOMO_AUTH_TOKENvsrc/lib/analytics/matomo.ts; token neníNEXT_PUBLIC_*, nepatří do klientského bundle a při chybě nebo chybějící konfiguraci vrací dashboard nulové fallbacky. - Admin dashboard může tato data číst přes
/api/admin/analytics; endpoint je přístupný jen pro přihlášené roleOWNERaSALON, vrací pouze agregované počty bez PII a při interní chybě spadne na bezpečný JSON fallback. - Pro dashboard je připravená klientská komponenta
src/components/admin/AnalyticsWidget.tsx; sama řešífetch('/api/admin/analytics'), loading, error a kompaktní souhrnVýkon webus návštěvami, rezervacemi a mírou rezervace. - Rozbalení
Zobrazit analytikuobsahuje hlavní funnelZobrazení -> Služba -> Termín -> Kontakt -> Odesláno -> Rezervacea mini sekciKvalita kontaktního kroku:zahájeno,fokus pole,začátek vyplnění,chyba pole+ procenta vůčiKontakt zahájen. - Admin přehled je kompaktní denní provozní cockpit: hlavní osa je
Provozní přehled -> Vyžaduje pozornost -> provozní KPI -> Dnešní plán / Nejbližší volné termíny, zatímco rychlé akce, týdenní souhrn a výkon webu jsou v pravém sloupci jako podpůrné bloky. Na desktopu je horní část záměrně nízká, aby byl bez dlouhého scrollu vidět hlavní provoz dne. - Hlavní dashboard CTA
Vytvořit rezervaciuž nevede jen do seznamu rezervací, ale rovnou otevírá drawer ruční rezervace (?create=1), takže kosmetička nezačíná další klik navíc. Dnešní plána kartaDalší rezervacena dashboardu nově ukazují i provozní mikroakceVolat,E-mailaNová rezervace, aby šlo obvolat klientku nebo rovnou navázat dalším termínem bez otevírání detailu bookingu.- Sekce
Nejbližší volné termínyuž neslouží jen jako přehled. Každé volné okno má akciPřidat rezervaci, která otevře admin drawer s předvyplněným dnem a časem. - Týdenní planner
Volné termínypřidal doInspektoru dnezkratkyPřidat rezervaci do dneaRezervovat vybraný blok. Otevírají stejný ruční booking drawer s předvyplněným dnem nebo konkrétním blokem, ale server pořád drží běžnou validaci kolizí a délky služby. - Detail rezervace v adminu nově umí vedle změny stavu, ceny, voucheru a termínu také akci
Změnit službu. Akce je určená pro situaci, kdy se na místě domluví jiný typ péče, ale rezervace má zůstat stejným bookingem. Změnit službuje povolené jen u čekajících a potvrzených rezervací. Server při uložení přepočítá délku služby, cleanup blokaci, ceníkový základ a ověří, že se nová služba stále vejde do rezervovaného času a nepere se se službovým voucherem.- Blok
Vyžaduje pozornostse vykreslí jen když existuje alespoň jeden actionable alert (emphasis !== ok); pokud nic nehoří, blok se úplně skryje. Pokud alerty existují, UI zvýrazní jeden primární alert (emphasis=primary, případně první dostupný) a ostatní drží jako kompaktní sekundární položky. - Pokud je blok
Vyžaduje pozornostzobrazený, alert text na mobilu se musí lámat do více řádků (beztruncate) a CTA může padat pod text; karta nesmí horizontálně přetékat mimo viewport. - Actionable alerty v
Vyžaduje pozornostaktuálně pokrývají jen skutečně akční provozní problémy: čekající rezervace na potvrzení, selhané e-maily a rezervace po konci termínu čekající na uzavření. - Absence slotů dnes/zítra není sama o sobě problém: sekce
Nejbližší volné termínypoužívá neutrální text (Momentálně nejsou publikované žádné nadcházející volné termíny.) a pokud existují draft sloty, zobrazí stav s počtem návrhů čekajících na publikování a odkazem do dostupnosti. - Rychlé akce v pravém sloupci nejsou primární místo pro vytvoření rezervace; hlavní CTA zůstává nahoře v
Provozní přehled. Pravý blok drží podpůrné vstupyRezervace,Dostupnost,KlientiaVouchery. - Detailní zdroje návštěv a funnel jsou v dashboardu až v rozbalení
Zobrazit analytiku, aby hlavní obrazovka nezobrazovala matoucí analytické hodnoty před provozními úkoly. - Sekce
Zdroje návštěvv tomto widgetu kombinuje Matomo kampaně a referrer typy do business názvůInstagram,Firmy,Google,Přímý vstupneboOstatní; rezervace u zdrojů jsou výslovně jen orientační odhad podle podílu návštěv na dokončenýchRezervace / Vytvořena, ne přesná atribuce. - Když je Matomo reporting rozbitý nebo zamčený, dashboard už neukazuje jen obecné nuly:
/api/admin/analyticsvrací i stav reportingu a widget vypíše provozní hlášku. Rychlá serverová kontrola funguje přesnpm run analytics:check. - V detailu owner
Email loguakceZnovu odeslat e-mailzaloží novýPENDINGlog s aktuálním e-mailem kontaktu jako nový pokus; historickýrecipientEmailpůvodního záznamu se kvůli auditu nikdy nepřepisuje. - Admin detail rezervace musí i při dlouhém jménu, e-mailu nebo hlášce po přesunu termínu zalamovat text uvnitř karet; success bannery, historie i key/value souhrny nesmí horizontálně přetékat mimo panel.
- Aktuální runtime stack podle
package.json:next16.3.1react19.2.8react-dom19.2.8prisma+@prisma/client7.9.1
- Veřejná část aktuálně pokrývá:
- homepage
- landing page
Dárkové voucheryna/voucherys CTA na domluvu voucheru a veřejné ověření kódu - služby a detail služby
- ceník rozdělený podle kategorií přes celou šířku obsahu
- o mně
- kontakt
- FAQ
- storno podmínky
- obchodní podmínky
- GDPR
- veřejné noindex ověření voucheru na
/vouchery/overeni
- Veřejný obsah je centralizovaný v
src/content/public-site.ts, aby šly texty a hlavní brand copy měnit bez zásahu do layout komponent. - Klientská copy veřejného webu má reflektovat, že salon provozuje jedna osoba. Jednotné číslo používej tam, kde mluví přímo provozovatelka (
doporučuji,pošlu,můžete mě kontaktovat); přirozené společné formulace s klientkou (společně doladíme) a studio jako místo (k nám) zůstávají v pořádku. - Mobilní veřejný header má ukázat všechny položky
mainNavigationjako čitelnou mřížku2 × 3a samostatné CTARezervace; cílem je zachovat úplnou orientaci bez mačkání textu do jedné řádky. - Veřejný header používá desktop navigaci až od
lg;mdvčetně iPad portrait zůstává v kompaktním tablet režimu (brand + CTA + mřížková navigace), aby se pravé CTA ani položky menu neusekávaly mimo viewport. - Hero sekce
/kontaktpoužívá samostatnou publikovanou fotku z media knihovny (CONTACT_PHOTO) jako pravý above-the-fold vizuál; pokud zatím není nahraná, zobrazí decentní placeholder a nesahá do fotek studia. - Globální SEO popis a fallback kontakty používají skutečné údaje PP Studia:
info@ppstudio.cz,+420 732 856 036aSadová 2, 760 01 Zlín; placeholder kontakty se nemají vracet ani při chybě DB settings. - Veřejné e-mailové odkazy používají
ObfuscatedEmailLink, ale výsledné HTML musí vždy obsahovat skutečnýmailto:odkaz; nepoužívej dočasnéhref="#", protože kontakt musí fungovat i před hydratací klientského JS. - Stručná komunikace storno pravidla na homepage a ve FAQ má být benefit-first a klientsky srozumitelná: nepoužívej interní názvy typu
storno oknoani procesní věty o tom, jak jsou pravidla komunikovaná; preferuj krátké formulace typuZměna nebo zrušení termínu je možné nejpozději 24 hodin předem.nebo kontextovou variantu se stejným významem. /storno-podminkyuž nepoužívá jen generický právní text; stránka má vlastní akční skladbuhero -> kontaktní box -> rychlý přehled pravidel -> krátké sekce, aby klientka během pár sekund viděla co dělat a jaké dopady má pozdní storno nebo no-show.- Copy na
/storno-podminkyje nyní záměrně měkčí: zdůrazňuje včasné oznámení a provozní dopad pozdního zrušení, ale automaticky nekomunikuje storno poplatek; zároveň výslovně odkazuje i na storno link v potvrzení rezervace a 24h reminderu. - FAQ na
/faquž není plochý seznam několika otázek; stránka používá skladbuhero s jemným CTA -> pravý informační box první návštěvy -> rychlá sekční navigace -> tematické accordion bloky. - Homepage i FAQ nově jemně promují samostatnou voucher landing page
/vouchery, ale hlavní priorita veřejného webu zůstává rezervace a orientace v péči. - Landing page
/voucheryuž není jen stručný teaser; obsahuje i rozhodovací scénáře pro výběr mezi konkrétní službou a hodnotovým voucherem a krátký průběh domluvy, aby pomáhala při reálném nákupním rozhodování. - FAQ copy je záměrně orientované na rozhodnutí před první návštěvou: řeší výběr služby, průběh první návštěvy, praktické detaily, komfort, organizaci i stručné storno shrnutí s odkazem na samostatnou stránku podmínek.
- FAQ odpovědi zůstávají serverově vypsané v HTML přes nativní
details/summary; JSON-LDFAQPagese skládá ze stejnéhoFaqSection -> FaqItemmodelu a nesmí obsahovat otázky, které nejsou na stránce reálně vidět. - FAQ pokrývá i praktické rozhodovací otázky před rezervací: objednání bez přesného výběru služby, potvrzení rezervace, úpravu péče podle stavu pleti, doporučenou frekvenci kosmetiky, příchod s make-upem, citlivou pleť, běžnou citlivost úpravy obočí, výdrž barvení obočí, dárkové vouchery, adresu studia a odkaz na parkování na
/kontakt#parkovani. - FAQ nově zahrnuje i konkrétní rozhodování mezi kosmetickým ošetřením, lash liftingem a laminací obočí, výdrž lash liftingu a laminace obočí, vhodnost návštěvy při podráždění očí a rozdíl mezi voucherem na službu a voucherem na hodnotu.
- Rezervační stránka
/rezervacemusí mít v HTML právě jeden hlavníh1nadpis pro veřejné booking flow; aktuálně je to textVyberte si termín, který vám nejlépe vyhovuje.nad samotným formulářem. - Reálné služby z DB dostávají veřejnou copy vrstvu v
src/features/public/lib/public-services.ts, ale zdrojem pravdy je modelService. - Katalogová i veřejná textová data (
name,slug, cena, délka, dostupnost, kategorie, pořadí,publicIntro,description,pricingShortDescription,seoTitle,seoDescription,idealFor,includes,benefits,goodToKnow) čte veřejný web z DB. - Služba má interní pole
cleanupMinutespro čas na úklid po službě. Hodnota má výchozí0, nastavuje se v admin detailu služby, klientce se nezobrazuje jako délka služby a používá se jen pro interní blokaci dostupnosti po skončení služby. - Veřejná i self-service rezervace nově vyžadují, aby se do publikovaného okna vešla samotná služba; cleanup blokace může přetéct za konec slotu. Navazující termíny se ale dál blokují až do
blockedUntil, takže další start se nabídne teprve po interním cleanup intervalu. - Stejné pravidlo platí i pro admin planner: seznam
Volná oknanesmí spoléhat jen naBooking.slotId, ale musí odečíst i cleanup blokaci přetékající do sousedního publikovaného slotu. - Stejné pravidlo teď drží i dashboard sekce
Nejbližší volné termíny: zobrazené řádky jsou souvislá volná okna po odečtení booking blokacíscheduledStartsAt -> blockedUntil, ne jen začátky jednotlivých publikovaných slotů se zdánlivě volnou kapacitou. - Když se rezervace zruší a okolo jejího slotu zůstanou sousední běžné publikované fragmenty bez aktivních rezervací, systém je nově při stornu automaticky sloučí zpět do jednoho souvislého slotu. Tím se dostupnost po starších bookinzích sama čistí a nevznikají trvalé zbytky typu
:15/:45, pokud už pro ně není reálný důvod. - V planner mřížce se cleanup zobrazuje žlutě uvnitř stále 30min buňky: při 15min overflowu obarví horní nebo dolní polovinu, při plné 30min cleanup blokaci je žlutá celá buňka. Druhá polovina buňky dál drží svůj stav (
zelenádostupnost,červenárezervace, případně jiné omezení) a legenda má pro cleanup samostatnou položkuÚklid. - Rezervační výběr služby používá DB
publicIntro; strukturovaný copy override podle slugu není trvalý zdroj obsahu a smí sloužit jen jako dočasný backfill zdroj. - Ceník na
/cenikmá vlastní modul vsrc/features/public/components/pricing-page.tsxa je rozdělený do jasné kompozicehero -> category chips -> hlavní sekce -> menší grid sekce -> finální CTA. - Katalog služeb a kategorií teď nese i veřejná pricing metadata:
- služba:
publicIntro,seoDescription,pricingShortDescription,pricingBadge(název je sjednocený v polinamepro web i rezervace) - kategorie:
pricingDescription,pricingLayout,pricingIconKey,pricingSortOrder; veřejný web i booking používají aktuálníServiceCategory.name
- služba:
/sluzby,/ceniki/rezervacemusí používat stejné mapování kategorií nad aktuálnímServiceCategory.namea stejné pořadí podlesortOrder.- Public pricing read model má runtime guard: jedna služba (
slug) smí být v ceníku právě jednou; duplicita přes více kategorií je validační chyba. - Homepage sekce
Doporučené službypoužívá ruční výběr z katalogu:Service.isFeaturedOnHomepage = trueahomepageSortOrder. Zobrazuje maximálně první tři aktivní veřejně rezervovatelné služby v aktivních kategoriích; pokud není vybraná žádná, zůstává fallback na první tři veřejné služby podle katalogového pořadí. - Admin sekce
SlužbyaKategorie služebtato metadata umí upravovat bez zásahu do databáze nebo kódu. - V klientských admin workspaces nad React
useOptimistic(např. rychlé akce kategorií) musí optimistic dispatch běžet uvnitřstartTransition(...); volání mimo transition vyhazuje runtime warning a degraduje UX při rychlých mutacích. - Admin sekce
Službyuž nepoužívá vysoké katalogové karty; seznam je nově seskupený podle kategorií a funguje jako hustší provozní workspace. - Každá skupina kategorií v adminu ukazuje počet služeb a jde rozbalit/sbalit; samotná služba má kompaktní řádek a sekundární kontext je až v rozbalení nebo v pravém detail draweru.
- KPI pás sekce
Službyje nyní čistě katalogový souhrn aktuálního běžného pohledu:Veřejné služby,Kategorie,Interní / skrytéaVyžaduje kontrolu. - Souhrnný řádek seznamu služeb drží jen provozní minimum
V seznamu / Skupin / Viditelné / Upozorněnía explicitně připomíná, že systémové/testovací položky zůstávají v běžném katalogu skryté. - Rychlé změny služby se v seznamu soustředí do malého menu
⋯; základní desktop řádek zůstává jednovrstvý a ukazuje jen název, délku, cenu, počet rezervací a badge stavu. - Toolbar sekce
Službyuž nepoužívá duplicitní mezihlavičku; nad seznamem zůstává jenPřehled služeb, jediné CTANová službaje v horní stránkové hlavičce a legenda stavů je schovaná do malého rozbalovacího prvku. - Ceník už nepoužívá vedlejší blok s poznámkami; detail služby zůstává místem pro doplňující vysvětlení.
- Veřejné stránky drží jednotný šířkový rytmus přes sdílený
Container(max-w-7xl); při úpravách layoutu nepřidávej další globální zúžení sekcí přesmx-auto max-w-*. - Vertikální spacing veřejných sekcí je sjednocený do rytmu
py-10 / sm:py-14 / lg:py-16; větší rozestupy používej jen pro obsahově výrazné bloky. - Rezervační vrstva stojí na ručně vypisovaných termínech přes
AvailabilitySlot, ne na pevné otevírací době. - Ruční rezervace v adminu nově dovoluje vytvořit klientku i bez e-mailu, což pokrývá rezervace z Instagramu, telefonu nebo osobní domluvy; pokud adresa chybí, klientské potvrzení se záměrně neposílá.
- Pending rezervace lze nově potvrdit nebo zrušit přímo z provozního e-mailu přes bezpečné jednorázové odkazy s mezikrokem potvrzení na veřejné route
/rezervace/akce/[intent]/[token]. - Admin sekce
Rezervacepoužívá nízkou stránkovou hlavičku s CTAPřidat rezervaci, jeden společný horní panel pro rychlé i detailní filtry a tenký KPI stripČeká na potvrzení / Dnes / Tento týden / Bez kontaktu. - Mobilní toolbar rezervací musí zůstat uvnitř pracovní karty: formulářové položky používají
min-w-0, nativní date inputy nesmí roztlačit grid a kompaktní admin panel má na telefonu menší boční padding. - Pracovní seznam rezervací je serverově seskupený do bloků
K uzavření,Čeká na potvrzení,NadcházejícíaMinulé.K uzavřeníje úplně nahoře a obsahuje aktivní proběhlé rezervace (ČekáneboPotvrzená), kterým už skončil termín, ale ještě nejsou uzavřené jakoHotovo,ZrušenáneboNedorazila. - Dlouhé seznamy rezervací používají progresivní odkrývání:
Minuléjsou výchozně sbalené, každá sekce ukazuje jen první várku položek a další rezervace se načítají přesZobrazit dalšíse zachováním filtrů i URL stavu. - Pole
Hledatv rezervacích nabízí živé našeptávání nad databází; při běžném textu nabízí hlavně klientky a služby, zatímco kontakty se ukážou jen u dotazu, který vypadá jako e-mail nebo telefon. Výběr dál používá stejné URL-driven hledání jako ruční text. - Pole
Hledatv rezervacích se po krátké pauze při psaní filtruje automaticky; tlačítkoFiltrovatzůstává jako explicitní fallback a výběr návrhu z našeptávače filtr použije hned. - Quick akce
Potvrditv seznamu rezervací nečeká na dokončení Pushover HTTP callu; stav rezervace se uloží a UI se odblokuje i při pomalé externí notifikační vrstvě. - Rezervace v adminu rozlišují
Kanál rezervacea marketingovéOdkud přišla:Webznamená, že rezervace vznikla přes veřejný booking flow, zatímco akviziční štítekInstagram,Google,Firmy.cz / SeznamneboDirect / bez kampaněvychází z UTM/referreru.Instagram zprávav kanálu rezervace je ručně zadaná rezervace z konverzace, ne webová UTM návštěva. - Akviziční cookie normalizuje primárně
utm_*, ale jako fallback přijímá imtm_source,mtm_mediumamtm_campaign, aby se booking attribution neztratil ani u Matomo-tagovaných odkazů. - Pracovní seznam drží sticky prvky jen tam, kde to dává smysl: na mobilu filtr bar scrolluje spolu s obsahem (nepřekrývá řádky), od
mdbreakpointu výš zůstává nahoře pro rychlou práci v delším seznamu; hlavička tabulky drží kontext a akce v řádku vrací okamžitý inline feedback přes loading stav a toast. - Mobilní pracovní seznam rezervací drží rychlé akce jako dva plnohodnotné dotykové ovladače vedle sebe (
Potvrdit,Otevřít); rychlé filtry lze horizontálně posunout bez zalomení štítků a formulářové akceFiltrovat/Zrušit filtrymají stejnou snadno dosažitelnou výšku. - V pracovním seznamu je teď nejvýraznější čas rezervace; uzavřené stavy
HotovoaZrušenámají menší vizuální váhu, inline akce se liší podle stavu rezervace a chybějící kontakt se zobrazuje neutrálně jakobez kontaktu. - Kontakt v řádku rezervace je praktický i na mobilu: telefon používá
tel:, e-mailmailto:a mobilní zobrazení skládá compact card s pořadímčas -> klientka -> služba -> stav. - Admin detail klientky na
/admin/klienti/[clientId]a/admin/provoz/klienti/[clientId]je provozní CRM karta: nahoře odpovídá kdo je klientka, jak ji kontaktovat, kdy byla naposledy a jestli má další termín; pod hlavičkou je kompaktníCRM souhrns poslední/příští návštěvou, hodnotou dokončených služeb, uhrazeno/neuhrazeno a rozpad rezervací.Neuhrazenoneukazuje budoucí aktivní rezervace jako dluh, ale jen doplatky u dokončených nebo už proběhlých aktivních rezervací. Historie návštěv a interní poznámka jsou vlevo, kontakt, zkrácený přehled klientky a tlumená metadata vpravo. - V seznamu klientek (
/admin/klienti,/admin/provoz/klienti) znamená sloupec i řazeníPoslední návštěvaposlední minulou dokončenou rezervaci (COMPLETED). Budoucí nebo ještě neuzavřený termín se do této hodnoty nepropisuje, i když profilovéClient.lastBookedAtuž může být aktualizované novou rezervací. - Stejné pravidlo platí i pro jednodušší legacy přehled klientek renderovaný přes
admin-section-page, aby se významPoslední návštěvamezi různými admin pohledy nerozcházel. - Starší URL
/admin/email-logyzůstává pro bookmarky funkční, ale přesměruje na pohled E-maily v/admin/logy?view=emails. - Generický admin fallback
AdminSectionPagebyl odstraněný. Každá reálná admin sekce teď má buď vlastní explicitní route soubor, nebo svou jasně pojmenovanou větev vcreateAdminSectionRoute(...). - Hlavní admin sekce používají sjednocený intro copy pattern: krátký kontext v eyebrow, jednoduchý pracovní název sekce a stručný provozní popis.
- E-maily jsou součástí společného přehledu Události a logy, takže s Událostmi, Pozorností a Systémem sdílejí filtry, řazení i stránkování.
- Kontakt klientky lze v detailu klientky upravit přímo v kartě
Kontakt; server action validuje e-mail/telefon, hlídá duplicitní e-mail a změnu propíše do profilu, aktivních rezervací (PENDING/CONFIRMED) a dosud neodeslaných e-mail logů navázaných na tyto rezervace. Proběhlé rezervace (COMPLETED,CANCELLED,NO_SHOW) se nepřepisují a každý propis do aktivní rezervace ukládá auditní stopu doBookingStatusHistorys metadaty původního/nového kontaktu. - Historie návštěv v detailu klientky u každé rezervace rozlišuje poznámky podle původu:
Klientkaukazuje poznámku z rezervace,Interněukazuje provozní poznámku týmu. Pokud existují obě, zobrazí se obě. - V seznamu klientek (
/admin/klienti,/admin/provoz/klienti) se detail klientky otevírá kliknutím na celý řádek/kartu; štítekDetailje součást stejné akce. - Primární akce
Vytvořit rezervaciv detailu klientky teď používá existující booking workspace/admin/rezervacenebo/admin/provoz/rezervaces query parametrycreate=1&clientId=...; ruční booking drawer se po otevření pokusí klientku předvyplnit, při neplatném ID zobrazí jemnou hlášku a při neaktivní klientce varování, ale formulář zůstává použitelný. - Po potvrzení rezervace zákaznice dostává v potvrzovacím e-mailu
.icspřílohu s jednou konkrétní kalendářovou událostí pro potvrzený termín, ne subscription feed. - Booking e-maily čtou kontaktní údaje salonu z admin nastavení (
SiteSettings): název, adresa, telefon a kontaktní e-mail se propisují do HTML i textové varianty. Pokud nastavení nebo DB nejsou dostupné, zůstávají bezpečné fallbacky PP Studia. - HTML náhledy hlavních booking a admin e-mailů lze vygenerovat příkazem
npm run email:previews; soubory se zapisují dotmp/email-previewsa jsou určené jen pro lokální vizuální kontrolu copy/layoutu. - Owner může v
/admin/nastaveninově zapnout chráněný Apple Calendar subscription feed na/api/calendar/owner.ics?token=...; feed je read-only, bere jen potvrzené rezervace a aplikace zůstává jediným source of truth. - OWNER muze v
/admin/nastaveniv blokuPushover notifikacenastavit vlastni Pushover User Key, zapnout/vypnout notifikace a zvolit event typyNova rezervace,Rezervace ceka na potvrzeni,Rezervace potvrzena,Rezervace zrusena,Termin presunut,Selhani emailu,Selhani reminderuaSystemove chyby. Blok je owner-only;SALONnema route ani navigaci do nastaveni a Pushover sluzba pred odeslanim znovu vybira jen aktivni DB uzivatele s roliOWNER. - U upozornění
Nova rezervaceje navíc uvedenoKlientka: Nova klientkaneboKlientka: Vracejici se klientka; rozhoduje, zda má klientka v databázi starší rezervaci. - Pushover aplikacni token je server-only env
PUSHOVER_APP_TOKEN; globalni vypinac jePUSHOVER_ENABLED=true. Chyba Pushover API se pouze loguje a nikdy nesmi rozbit booking, email worker ani reminder flow. - Pushover kod je rozdeleny na Next.js wrapper
src/lib/notifications/pushover.tsseserver-onlymarkerem a worker-safe implementacisrc/lib/notifications/pushover-core.ts, kterou smi nacitat standaloneemail:workerprestsx. - Event
Systemove chybyuz realne pokryva vic nez jen verejne vytvoreni rezervace: owner alert se posila i pri neocekavane chybe self-service i admin presunu terminu, pri rucnim vytvareni rezervace, pri schema driftu verejneho booking flow, pri selhani planner mutaci volnych terminu, pri kritickych voucher akcich/emailu, pri padu DB health checku, pri failu enqueue navazneho reschedule emailu a kdyz/api/admin/analyticsnebo owner resend invite API spadne do fallback/error stavu. - Admin detail rezervace nově podporuje samostatnou akci
Přesunout termín; booking zůstává stejným záznamem, ale změna projde backend validací, auditním logem, resetem reminder návaznosti a volitelným klientským e-mailemTermín byl změněn. - Drawer
Přesunout termínv admin detailu při skládání volných slotů nepočítá právě upravovanou rezervaci jako obsazenost. Pokud je před původním začátkem 30min publikované okno a délka služby se vejde přes toto okno plus vlastní původní slot, nabídne se posun na dřívější začátek. - Hlavička detailu rezervace je statická (není plovoucí) na všech breakpointech, aby nepřekrývala rozhodovací panel a CTA při potvrzení služby.
- Horní hlavička detailu rezervace je záměrně nízká a dvouřádková: první řádek drží návrat, stav/kanál a rychlé akce, druhý řádek jméno klientky + službu, délku a termín jako kompaktní text (bez velkého termínového boxu).
- Pokud je vyplněná klientská nebo interní poznámka, detail rezervace ji zvýrazní badge štítkem už v hlavičce i v samotném panelu
Poznámky, aby byla okamžitě viditelná. - Admin detail rezervace už nefunguje jako dlouhá informační stránka; nově je to rychlý rozhodovací panel se statickou kompaktní hlavičkou, horním akčním blokem, kompaktním souhrnem v bočním sloupci a sjednoceným blokem poznámek. Na mobilu jde souhrn hned pod hlavičku, potom teprve
Další krok,Úhrada, poznámky a historie. - Panel
Další krokje pracovní cockpit. U potvrzené rezervace má copy jasně říct, že termín je potvrzený a po návštěvě se má uzavřít jako hotový, případně označit jakoNedorazila. Primární provozní CTA jeDokončit návštěvu;Přesunout termínaNedorazilajsou sekundární kroky.Zrušit rezervacipatří do samostatné sekceNebezpečná akce / Zrušení rezervaces červeným varováním, důvodem a potvrzovacím tlačítkem, nikdy vedle hlavního provozního CTA. - Pro provozní realitu salonu je hlavní CTA u potvrzené rezervace přejmenované na
Dokončit návštěvu. Cockpit ukazuje platební kontext (DoplatekneboPlatba vyřešena) a při doplatku nabízí kompaktní completion flow:Hotově,QR platba,Voucher,KombinovaněneboBez platby. - Completion flow při doplatku zapisuje úhradu/voucher přímo v rámci dokončení návštěvy a pak přepne stav na
Hotovo. Zvolená platba nebo voucher musí pokrýt celý doplatek; částečný voucher nechá návštěvu otevřenou, pokud admin vědomě nezvolíBez platby. Bez platbyje vědomá výjimka, vyžaduje povinný důvod a rezervace může zůstatHotovoi s doplatkem.- Kompaktní varianta detailu rezervace zkracuje vertikální výšku: horní cockpit používá krátký stavový řádek (
Potvrzený termín · Po návštěvě uzavři rezervaci.), potvrzení akce je v jednom kompaktním řádku (vysvětlení + volitelný důvod + potvrzení) a sekceNebezpečné akceje výchozně sbalená. - Success potvrzení po akci
Přesunout termínmusí v paneluDalší krokzůstat uvnitř své sekundární karty: grid item potřebuje dovolit shrink (min-w-0) a banner musí text zalamovat, jinak dlouhé datum/důvod přeteče přes completion flow. - Admin detail rezervace má panel
Úhradas jasnou prioritouStav úhrady -> Cena k úhradě -> doplatek -> + Zapsat platbu -> Přehled úhrad. Horní souhrn zůstává nejvýraznější a vždy ukazuje stav (Bez úhrady/Částečně uhrazeno/Zaplaceno/Přeplaceno), cenu k úhradě, uhrazeno celkem, voucher, mimo voucher a doplatek nebo přeplatek; právě doplatek je opticky nejdůležitější hodnota. Úprava individuální ceny se otevírá kompaktně přesUpravitpřímo u položkyCena k úhradě; samostatný blokCena rezervacese ve výchozím zobrazení nepoužívá. CTA+ Zapsat platbuje dobře viditelné, ale nesmí přebít hlavní akciDokončit návštěvu; seznam existujících plateb se zobrazuje jen jednou vPřehled úhrad, kde je dostupné i smazání platby. Voucher se samostatně neuplatňuje a prázdný stavPřehled úhradpoužívá jemnou informaciŽádné úhrady zatím nejsou evidované. - V kompaktním režimu
Úhradazačíná stručným trio souhrnemDoplatek / Uhrazeno / Voucher; podrobnější platební historii je možné rozbalit přesDetail úhrady. - Samostatná sekce
Úhradazůstává beze změny role: slouží pro dodatečné zápisy plateb, opravy, úpravy ceny a nestandardní situace mimo completion flow; skutečné voucherové čerpání se zde samostatně neprovádí. OWNERiSALONmohou v paneluÚhradanastavit individuální cenu rezervace přesUpravit cenu. Prázdná hodnota nebo stejná částka jako ceníkový snapshot úpravu zruší; rozdílná cena vyžaduje důvod. Sleva ani navýšení nejsou platba, ale mění cenu k úhradě, ze které se počítají hodnotové vouchery, běžné platby, doplatek i CRM souhrn. Službový voucher je nárok na konkrétní službu; při uplatnění se řídí shodou služby, ne individuální cenou rezervace.- Platební část
CRM souhrnuv detailu klientky používá stejný helpergetBookingPaymentSummary(...)jako detail rezervace.Uhrazenosčítá skutečně zapsané platby a voucherová čerpání, včetně případných úhrad zapsaných předem.Neuhrazenose ale počítá jen z rezervací ve stavuCOMPLETEDnebo z aktivníchPENDING/CONFIRMEDrezervací se začátkem v minulosti; budoucí aktivní,CANCELLEDaNO_SHOWrezervace se do doplatku nezapočítávají. - Platby mimo voucher se zapisují přímo v panelu
ÚhradametodamiHotově,Kartou,Převodem / QRaJiné.OWNERiSALONmohou platbu zapsat, mazání platby je dostupné jen proOWNER. QR kód se v této verzi negeneruje, textPřevodem / QRje pouze popisek platební metody. - Pokud rezervace už nese intended voucher z veřejného flow, detail rezervace zobrazí tento záměr přímo v panelu
Úhrada; samostatný formulář pro uplatnění se nezobrazuje. Skutečné čerpání použije kód v completion flow v sekciDalší krok. - Pokud je hodnota voucheru nižší než cena služby, voucher pokryje jen svůj zbývající zůstatek a zbytek ceny se řeší jako doplatek mimo voucher. Completion flow zobrazí maximální použitelnou částku, při ručním zadání ji omezí na dostupný zůstatek a po úspěchu informuje o zbývajícím doplatku.
- Veřejné booking flow v kontaktním kroku nabízí volitelné pole
Kód voucheru. Pokud je prázdné, rezervace pokračuje beze změny; pokud je vyplněné, server kód při vytvoření rezervace ověří a uloží ho jen jako intended voucher naBooking. - Skutečné čerpání voucheru v provozu vzniká pouze v serverové akci
completeBookingVisitAction(), která v jedné transakci dokončí návštěvu a zapíšeVoucherRedemption; samostatná kompatibilní admin akce je pouze odmítající pojistka. Samotné veřejné zadání nebo intent naBookingzůstatek nikdy neodečítá. - Veřejná stránka
/vouchery/overenislouží jen ke kontrole platnosti kódu z poukazu nebo QR odkazu/vouchery/overeni?code=.... Formulář vždy dovolí kód změnit a znovu ověřit; výsledek se počítá server-side po normalizaci kódu. Po platném výsledku stránka nabízí pouze další krok na rezervaci termínu nebo napsání do studia, žádné uplatnění voucheru. - Po otevření detailu je během pár sekund vidět klientka, služba, termín, stav a nejpravděpodobnější další akce; reschedule zůstává oddělený jako samostatný drawer a chování pro
OWNERiSALONje stejné. - Veřejný manage flow
/rezervace/sprava/[token]má nově DB integrační coverage nad reálným Prisma wiringem; testy ověřují token access, self-service storno, self-service přesun i hlavní auditní a notifikační side effects bez browser E2E vrstvy. - Self-service přesun přes
/rezervace/sprava/[token]po úspěchu záměrně nerevaliduje právě otevřenou veřejnou route; v Next.js 16 by route refresh po server action mohl přemountovat klientský panel a smazat lokální success stav dřív, než se ukáže uživatelce. - Veřejná stránka změny termínu je UX refaktorovaná do toku
kontext -> aktuální rezervace -> hybridní výběr termínu -> potvrzení -> sekundární storno: nejbližší termíny jsou nahoře jako rychlé chips, kalendář slouží jako sekundární výběr dne a potvrzení je jediná dominantní CTA. - Při self-service přesunu se aktuální rezervace vynechává z výpočtu obsazenosti katalogu, aby šel termín posunout i na dřívější začátek v publikovaném volném bloku před původním začátkem, pokud celý nový interval pořád pokrývá souvislý publikovaný řetězec a nekoliduje s jinou aktivní rezervací.
- Veřejná booking dostupnost už není omezená jen na jeden fyzický
AvailabilitySlot; pokud na sebe publikované sloty bez mezery navazují a mají stejná veřejná pravidla, katalog je složí do jednoho delšího okna a delší služby se tak mohou rezervovat i přes více sousedních segmentů. - Uvnitř takto složeného okna systém stále drží správný podkladový
slotIdpro vybraný start času a backend při vytvoření nebo přesunu termínu ověřuje celý souvislý řetězec segmentů bez mezer, blokací a kapacitních kolizí. - Ve veřejném rezervačním formuláři klik na den v kalendáři přesouvá fokus na blok
Dostupné časy; teprve výběr konkrétního času potom přesune uživatelku do kontaktního kroku. - Kontaktní krok veřejného booking flow musí držet explicitní vazby polí na popisky, nápovědy a chybové texty (
id,htmlFor,aria-describedby) a chybové hlášky oznamovat přes live regiony nebo alerty; viditelnýfocus-visiblestav polí a odkazů má zůstat v akcentu PP Studia. - Když rezervace využije jen část takového navazujícího řetězce, engine nově rozseká i krajní segmenty coverage, aby admin planner ukazoval skutečně volné okraje jako normální dostupnost a ne jako technicky chráněný zbytek slotu.
- Pro starší dev/legacy data existuje jednorázový repair helper
scripts/repair-legacy-chained-slots.mjs. Nejdřív ho spusť bez parametrů jako dry-run; teprve pokud vypíše bezpečnérepairablepřípady, můžeš použít--apply. Případy s více bookingy na jednom anchor slotu zůstávají záměrně veskippedrežimu. - Implementačně je veřejný booking flow po stabilizačním refaktoru rozdělený do menších interních komponent (
progress panel,service step,term step,contact step,summary sidebar), ale chování pro klientku zůstává stejné. - Admin má dva směry použití:
- full admin na
/admin/*pro roliOWNER - lite admin na
/admin/provoz/*pro roliSALON
- full admin na
- Obě rozhraní sdílejí stejné doménové entity, ale liší se navigací i hustotou UI:
OWNERvidí strategické a technické sekce navícSALONvidí jen provozní sekce a jednodušší copy bez technických pojmů
- Přesun termínu má pro
OWNERiSALONstejné chování; role mění jen administrativní cestu, ne business logiku reschedule flow. - Filtrační lišta sekce
Službyje na desktopu sticky a zůstává během scrollu po ruce; horní statistiky jsou záměrně menší, aby nepřebíraly roli hlavního obsahu. Scope běžného katalogu se v toolbaru komunikuje jen přes malé pill stavy typuBěžný katalogaSystémové skryté. - Sekce
Volné termíny / Týdenní plán dostupnostídrží grid-first provozní workflow: horní hlavička je nízká, datum týdne se ukazuje jen v planner toolbaru a pravý panel je zhuštěný do blokůInspektor dneaDetail výběru. - V planneru má legenda stavů zůstat sekundární a sbalená u detailu výběru; čitelnost času se zvyšuje spíš kontrastem levé osy, jemným zvýrazněním celých hodin a jasnějším selected stavem než dalšími vysvětlovacími kartami.
CANCELLEDrezervace už v týdenním planneru nemá působit jako barevná nebo editační překážka. Historie zrušené rezervace se zachová v archivovaném slotu na pozadí, ale samotná mřížka má pro obsluhu ukazovat jen reálně důležitou dostupnost, aktivní rezervace a omezení.- Týdenní planner dostupností a veřejná booking service vrstva jsou po stabilizačním refaktoru modulární i v kódu, ale bez změny URL, exportů nebo databázového modelu.
- Prisma schema v1 už pokrývá:
- admin uživatele a role
- kategorie služeb a služby včetně samostatné veřejné rezervovatelnosti
- sloty s omezením na vybrané služby
- klienty, rezervace a historii stavů
- dárkové vouchery (
Voucher,VoucherRedemption) včetně admin evidence, admin uplatnění a veřejného intended zadání při rezervaci - e-mailové logy, action tokeny, legacy
Setting, singletonSiteSettingsa metadata modelMediaAsset
- Voucher systém má připravenou serverovou business vrstvu, admin evidenci a provozní detail:
- hodnotový voucher (
VALUE) drží původní a zbývající hodnotu v Kč a může být čerpaný postupně, - voucher na službu (
SERVICE) drží snapshot služby v okamžiku vydání a po admin uplatnění se celý označí jako uplatněný, - veřejná validace voucheru při rezervaci pouze ověřuje použitelnost pro vybranou službu a nic neodečítá,
- skutečné čerpání vzniká pouze admin/server akcí, která zapisuje
VoucherRedemption; jedna rezervace smí mít nejvýše jeden uplatněný voucher.
- hodnotový voucher (
- Admin evidence voucherů je dostupná pro
OWNERna/admin/voucherya proSALONna/admin/provoz/vouchery. - Předtištěné vouchery mají samostatnou evidenci na
/admin/vouchery/predtistene(OWNER) a/admin/provoz/vouchery/predtistene(SALON). OWNER zde založí sérii 1–500 kusů podle aktivní šablony a stáhne vícestránkové tiskové PDF; PDF je dostupné pouze do fyzického převzetí série. Po označeníPřijatose kusy stanou dostupnými k prodeji, stejnou sérii už nelze znovu tisknout a pro další fyzický tisk je potřeba založit novou sérii. - Prodejní aktivace je dostupná přes
Aktivovat vouchernebo/admin/vouchery/aktivovata/admin/provoz/vouchery/aktivovat. Obsluha zadá kód z předtištěného kusu, vybere hodnotu nebo výslovně konkrétní službu a potvrdíPotvrdit prodej a aktivovat; před zápisem ještě potvrdí nevratnost akce. QR kód předtištěného kusu slouží k veřejnému ověření, neotevírá přímo administrační aktivaci. PřihlášenémuOWNERneboSALONse u dostupného kusu na ověřovací stránce zobrazíAktivovat tento voucher, které otevře standardní aktivaci s předvyplněným kódem. Aktivace vytvoří běžný aktivní voucher se stejným kódem a uloží vazbu na skladový kus; opakované odeslání je bezpečně idempotentní. - Předtištěný voucher lze v provozu aktivovat také takto: na iPhonu otevřete aplikaci Fotoaparát, naskenujte QR na voucheru, otevřete ověřovací stránku a po přihlášení dokončete standardní aktivaci přes
Aktivovat tento voucher. QR scan voucher sám neaktivuje. - Dostupný kus lze označit jako
VOIDs důvodem. Při uzavření série se všechny zbývající kusy automaticky zneplatní, aktivované kusy zůstanou zachované a každý přechod série/kusu se zapisuje do auditní historie. Veřejné ověření před aktivací zobrazí pouze obecnou informaci, že voucher zatím nebyl aktivován. - Seznam voucherů je na desktopu kompaktní tabulka se sloupci
Kód,Typ,Voucher,Čerpání / zůstatek,Stav,PlatnostaAkce; na menších šířkách přechází do nízkých karet. - Horní část seznamu používá nízkou stránkovou hlavičku s CTA
Nový vouchervpravo a ještě nižší metric stripVoucherů celkem / Aktivní / Částečně čerpané / Uzavřené; filtrq / type / statusje dál URL-driven. - KPI pás seznamu voucherů je provozní souhrn, ne jen evidence stavů: ukazuje
Zbývá k uplatnění,Otevřené vouchery,Brzy expirujíaUzavřené. SoučetZbývá k uplatněnízahrnuje jen otevřené vouchery. - Stavové badge voucherů patří přímo do sloupce
Stav;Aktivníje zelený,Částečně čerpanýzlatý,Uplatněnýtlumený aPropadlývarovný. - Všechny voucher routy včetně detailu a vytvoření běží uvnitř standardního tmavého admin shellu; pokud voucher obrazovka vypadá světle nebo bez navigace, chybí příslušný route layout.
- Seznam voucherů podporuje hledání podle query parametru
q, filtr typutype=all|value|servicea filtr stavustatus=all|active|partially_redeemed|redeemed|expired|cancelled|draft. - Nový voucher lze vytvořit přes
/admin/vouchery/novynebo/admin/provoz/vouchery/novy. Formulář podporuje hodnotový poukaz s částkou v Kč a poukaz na aktivní službu se snapshotem názvu, ceny a délky. Admin UI se soustředí na evidenci kupujícího a jeho e-mailu; obdarovaný a věnování se v této verzi ve formuláři nezobrazují. Pravý souhrn slouží jen jako živý provozní náhled před uložením. - Detail voucheru je dostupný na
/admin/vouchery/[voucherId]a/admin/provoz/vouchery/[voucherId]. Nahoře má jednu kompaktní summary kartu s kódem, typem, stavem, platností, čerpáním a akcemiDigitální PDF,Tiskové PDFaPoslat e-mailem. Pod ní je dvousloupcový provozní layout: vlevoParametry voucheru,Historie uplatněnía provozní úpravy, vpravoKupující a odeslánía nízká sekcePoslední e-mailové pokusy.Digitální PDFvede na/admin/vouchery/[voucherId]/pdfnebo/admin/provoz/vouchery/[voucherId]/pdfa vrací 210 × 99 mm;Tiskové PDFna/pdf/tiskzachovává bleed 216 × 105 mm a TrimBox 210 × 99 mm. Obě varianty používají uložený vzhled voucheru a e-mail přikládá digitální variantu. - V detailu voucheru lze upravit jen provozní údaje kupujícího (
purchaserName,purchaserEmail), platnost do a interní poznámku. Kód, typ, hodnota, měna, služba, čerpání a PDF identita se běžnou editací nemění. Voucher lze zrušit akcíZrušit voucher, která vyžaduje důvod, nastaví stavCANCELLEDa zachová auditní stopu; zrušení je povolené jen u voucheru bez čerpání. - V detailu voucheru je nova manualni akce
Poslat e-mailem. Odeslani se spousti vyhradne explicitnim submittem formulare v adminu; nevznika automaticky pri vytvoreni voucheru ani na verejnem webu. - Formular
Poslat e-mailempredvyplni prijemce zpurchaserEmail(pokud existuje) a predmetDarkovy poukaz PP Studio. Telo e-mailu je zamerne pevne podle schvalene sablony; obsluha ho neupravuje. - Poslat e-mailem lze pouze pro efektivni stavy voucheru
ACTIVEaPARTIALLY_REDEEMED. StavyDRAFT,REDEEMED,EXPIREDaCANCELLEDjsou server-side odmítnute s hlaskouVoucher v tomto stavu nelze odeslat e-mailem. - Voucher email se loguje do
EmailLog(typVOUCHER_SENT) a pouziva stavajici email worker:EMAIL_DELIVERY_MODE=background: záznam jde do fronty a worker ho odesle s retry politikou.EMAIL_DELIVERY_MODE=log: záznam se oznaci jako odeslany v log rezimu bez SMTP odeslani.
- Email worker pro PDF prilohu importuje worker-safe
src/features/vouchers/lib/voucher-pdf-core.ts; Next.js wrappersrc/features/vouchers/lib/voucher-pdf.tszustava jen pro admin routy simport "server-only". - Email vzdy obsahuje jen bezpecna data (typ, hodnota nebo sluzba, kod, platnost, overovaci URL, instrukce) a PDF prilohu
voucher-KOD.pdf; nikdy neobsahujeinternalNote, historii cerpani ani technicka ID. - PDF voucheru obsahuje grafický základ z verzovaného PDF masteru včetně loga, adresy, webu a ostatních pevných textů a pouze dynamická voucherová data: hodnotu nebo název služby, platnost, kód voucheru a QR kód. Neobsahuje jméno ani e-mail kupujícího, interní poznámku, historii uplatnění ani technická ID. Voucher templates nejsou spravovány přes Media Manager.
- Veřejné ověření voucheru na
/vouchery/overenijenoindexa není v sitemap. Platný voucher ukazuje jen bezpečná pole: kód, typ, zbývající hodnotu uVALUE, název služby ze snapshotu uSERVICEa platnost do. Platný stav doplňuje CTARezervovat termínna/rezervacea e-mailovéNapsat do studia. Neplatný voucher ukazuje pouze obecné bezpečné důvody: nenalezený, zatím neaktivní, uplatněný, propadlý, zrušený nebo bez dostupného zůstatku. - Veřejné ověření voucheru má server-side rate limit podle IP hashe (okno 10 minut, max 10 pokusů). Při překročení vrací jen obecnou hlášku o dočasném omezení; neprozrazuje interní detail ani existenci konkrétního kódu.
- Veřejné ověření voucher nikdy neuplatňuje: nevytváří
VoucherRedemption, neměníremainingValueCzkaniVoucher.status. - Stav
Propadlýv admin seznamu vychází z aplikačního efektivního pravidla: aktivní nebo částečně čerpaný voucher povalidUntilse zobrazuje a filtruje jako propadlý, i když DB status ještě neníEXPIRED. - Platnost voucheru se pro provozní stav i veřejné ověření vyhodnocuje po pražských kalendářních dnech. Voucher s dnešním datem
Platnost odje aktivní okamžitě; uložený čas v databázi nemění význam data.
npm install
cp .env.example .env
npm run db:generate
npm run devPokud next dev spadne na Turbopack cache chybu typu Failed to restore task data nebo chybějící .sst soubor v .next/dev/cache/turbopack, spusť:
npm run dev:cleanFallback režim bez Turbopacku:
npm run dev:webpacknpm test nyní spouští i DB-backed integrační testy (nejsou skipnuté), takže běžná verifikace zahrnuje i booking integrační scénáře.
Browser E2E vrstva používá Playwright a spouští se samostatně:
npm run test:e2eE2E testy nejdřív vytvoří produkční build, startují lokální next start server na samostatném portu, přepínají e-mail delivery do log režimu a seedují izolovaná data pro veřejnou rezervaci, storno, přesun termínu a admin potvrzení rezervace. Booking fixture rozkládá termíny podle runId do širšího rozpětí budoucích dní a časů, aby stale aktivní E2E rezervace ze staršího nedočištěného běhu náhodně neobsadily stejný veřejný čas. Self-service přesun termínu má v Playwrightu cíleně o něco širší timeout, protože čeká na plný server action roundtrip nad produkčním buildem; pokud je i „úspěšný“ náhradní slot mezitím obsazený paralelním během, test fallbackově zkusí další dostupné sloty a čeká na skutečný success heading. Při prvním spuštění na novém stroji může být potřeba doinstalovat Playwright browser přes npx playwright install chromium. Na Node 24 držíme Playwright alespoň na řadě 1.60+; starší 1.59.1 se v GitHub Actions uměla zaseknout při rozbalování browser downloadu po dosažení 100 %.
Build pro Playwright musí běžet se stejným NEXT_PUBLIC_APP_URL jako následný PLAYWRIGHT_BASE_URL/next start (http://127.0.0.1:3100, pokud nenastavíte jinak). NEXT_PUBLIC_* hodnoty se inlinují už při buildu; když se build udělá třeba na interním hostu serveru a test běží na 127.0.0.1:3100, admin login redirect pošle browser na špatný origin a E2E skončí ERR_CONNECTION_REFUSED. Stejně tak musí build dostat testovací NEXT_PUBLIC_MATOMO_* hodnoty, protože Playwright smoke kontroluje Matomo _paq ve výsledném klientském bundle. Veřejné canonical URL přitom dál držíme přes NEXT_PUBLIC_SITE_URL=https://ppstudio.cz.
Admin smoke scénáře pro OWNER/SALON role mají záměrně vyšší timeout 90_000 ms, protože kontrolují více admin sekcí za sebou a v CI mohou čistě kvůli délce průchodu přesáhnout výchozích 45_000 ms.
Reschedule E2E scénář po ověření runtime kolize přepíná na předem určený nekolizní slot z fixture, ne na "další" tlačítko podle pořadí. Při rozšiřování fixture proto drž explicitní cílové labely nebo ISO start časy a zachovej run-specific rozptyl termínů, aby test zůstal order-independent a stabilní v CI i lokálně.
Pokud selže scénář client can reschedule a booking through a public token, test vypíše i poslední rozpoznaný stav formuláře (konflikt slotu, validační hláška nebo obecná chyba), takže je rychlejší odlišit timing flake od skutečné doménové regrese.
GitHub Actions CI používá stejnou verifikační sadu automaticky na pull requestech a pushech do hlavních větví:
npm run lint
npm test
npm run build
npm run test:e2eWorkflow si pro testy startuje PostgreSQL service container a používá bezpečné lokální/testovací env hodnoty bez reálného SMTP odesílání.
Pro cílené spuštění pouze booking DB integračních testů je připravený i samostatný příkaz:
npm run test:db:bookingAktuálně pokrývá i novější booking DB scénáře nad stejným globem src/features/booking/lib/*.integration.test.ts, mimo jiné:
src/features/booking/lib/booking-rescheduling.integration.test.tssrc/features/booking/lib/booking-management.integration.test.tssrc/features/booking/lib/booking-manual.integration.test.tssrc/features/booking/lib/booking-public-voucher.integration.test.tssrc/features/booking/lib/booking-email-worker.integration.test.ts
U DB integračních seedů dostupnosti nepoužívej úzké fixní časové okno; při paralelním běhu CI to může náhodně kolidovat na AvailabilitySlot_active_time_window_excl. Bezpečnější je čas odvodit z UUID/hash rozptylu uvnitř booking window.
Pro rychlé unit ověření veřejné správy rezervace a reschedule pravidel jsou v repozitáři i cílené testy bez DB:
node --import tsx --test src/features/booking/lib/booking-management.test.ts
node --import tsx --test src/features/booking/lib/booking-rescheduling.test.tsPokud databáze ještě neobsahuje schema nebo přibyly nové migrace:
npm run db:migrate- Upload root se nastavuje přes
MEDIA_STORAGE_ROOT. - Kanonická
/media/public/*i legacy/media/*URL sdílejí jediný handlersrc/lib/media/public-media-route.ts; ten zpřístupní pouze publikovanýMediaAsset. - Pokud proměnná není vyplněná, aplikace použije výchozí cestu
/var/www/ppstudio/uploads. - Uvnitř rootu aplikace odděluje:
public/pro veřejně čitelná médiatemp/pro budoucí drafty nebo přechodné upload workflow
- Veřejná média se zobrazují přes URL vrstvu
/media/public/<type>/YYYY/MM/<filename>, ne přímým odkazem na fyzickou cestu. - Statické assety verzované v repozitáři (
public/brand) a uploady z adminu jsou dvě rozdílné věci; nové admin obrázky mají jít přes media storage vrstvu. - Pro JPEG/PNG/WebP uploady nyní vzniká lehká server-side image pipeline přes
sharp:- originál se při zápisu normalizuje přes EXIF auto-rotate a ukládá se jako
{assetId}-original.<ext> optimizedvarianta se generuje s auto-rotate podle EXIF, max šířkou 1920 px a rozumnou kompresíthumbnailvarianta se generuje pro admin grid s cílovou šířkou kolem 400 px- veřejný web čte
optimizedUrl, admin grid čtethumbnailUrla starší média bez variant padají zpět na původníurl
- originál se při zápisu normalizuje přes EXIF auto-rotate a ukládá se jako
- Next.js 16 v dev režimu blokuje cross-origin přístup k dev assetům a HMR endpointům, pokud origin výslovně nepovolíš.
- Projekt proto v
next.config.tspovolujeallowedDevOriginspro lokální host192.168.0.143i veřejnou doménuppstudio.cz/www.ppstudio.cz, aby šel dev server otevřít i přes Synology reverse proxy nebo z jiného zařízení v síti. - Po změně
allowedDevOriginsje potřeba restartovatnpm run dev. - Pokud budeš používat jiný hostname nebo IP, doplň ho do
allowedDevOriginsa změnu zapiš i do dokumentace.
- Import běží přes JSON soubor a upsertuje záznamy podle
slug. - Nejrychlejší postup:
node scripts/import-services.mjs --file scripts/services.import.example.json --dry-run
node scripts/import-services.mjs --file path/to/old-web-services.json- Import očekává strukturu:
categories[]s poliname,slug,description,pricingDescription,pricingLayout,pricingIconKey,sortOrder,pricingSortOrder,isActiveservices[]s poliname,slug,categorySlug,publicIntro,seoDescription,pricingShortDescription,pricingBadge,durationMinutes,priceFromCzk,description,shortDescription(legacy, volitelné),publicName(legacy, volitelné),sortOrder,isActive
- Pokud starý web exportuje data v jiném formátu, je potřeba je před importem namapovat do této struktury.
- Pro tvoje aktuální kategorie je připravený vzor v
scripts/old-web-categories.json. - Pro tvoje aktuální služby je připravený vzor v
scripts/old-web-services.json.
- Produkční DB se sama nezmění pouhou úpravou seedu nebo souboru
src/features/public/lib/service-copy-overrides.ts; veřejný web bere obsah služeb z DB. service-copy-overrides.tsje jen dočasný migrační zdroj pro naplnění nových políseoTitle,idealFor,includes,benefitsagoodToKnow.- Pro bezpečný backfill nových polí existuje ruční skript:
npm run db:backfill-service-copy -- --dry-run
npm run db:backfill-service-copy -- --confirm- První příkaz je dry-run: vypíše počet známých služeb, nalezené DB záznamy a pole, která by se měnila.
- Ostré spuštění vyžaduje
--confirma před ním je povinná aktuální záloha produkční DB. - Skript hledá pouze známé služby podle stabilního
slug, zastaví se při chybějícím známém slugu a nemění žádné neznámé služby. - Aktualizuje jen
Service.seoTitle,Service.idealFor,Service.includes,Service.benefitsaService.goodToKnow; nemění ID, slug, název, cenu, délku, pořadí, aktivitu, veřejnou rezervovatelnost ani kategorii. - Starší textová pole
publicIntro,description,pricingShortDescriptionaseoDescriptionupravuj přes admin nebo samostatně kontrolovaným DB updatem.
- Pro rychlý úklid testovacích provozních dat použij:
npm run db:clear-booking-data
npm run db:clear-booking-data -- --confirm- Skript v první fázi jen vypíše počty rezervací, slotů a navázaných logů. Ke skutečnému smazání je potřeba explicitní
--confirm. - Mažou se
Booking,AvailabilitySlot, jejich tokeny, historie, navázané e-mailové a submission logy a nakonec i osiřelé klientky bez aktivní vazby na rezervaci. - Katalog služeb a kategorií, admin účty,
SiteSettings,CalendarFeedaniMediaAssetse nemažou.
- Navigace vede na klíčové konverzní a důvěryhodnostní stránky místo jedné přetížené homepage.
- Detail služby je renderovaný v request-time režimu nad DB katalogem služeb, takže změny z adminu nečekají na nový build. Nad hero sekcí má viditelnou drobečkovou navigaci
Domů -> Služby -> Název službya sekundární odkaz zpět na přehled všech služeb. /rezervacenyní obsahuje produkční V1 flow:- výběr kategorie služby a následně konkrétní služby
- výběr konkrétního času v rámci ručně publikovaného volného okna
- kontaktní údaje klienta
- souhrn a potvrzení
- Výběr služby je nově rozdělený na dvě úrovně (
kategorie -> služba), aby se klientka rozhodovala v kratších blocích a rychleji našla správnou variantu. - Po výběru služby booking flow automaticky scrolluje na krok s termíny.
- Výběr termínu v kroku 2 teď začíná sekcí
Nejbližší dostupné termíny; kalendář zůstává hned pod ní jako sekundární cesta pro jiný den. - Krok 2 nabízí starty po 30 minutách uvnitř dostupného okna a bere v úvahu délku služby i už obsazené intervaly.
- Krok
Vyberte termínpoužívá větší a výraznější tlačítka časů s menší hustotou na řádek; detail termínu (konec,délka, případná poznámka) zůstává v souhrnu. - Kontaktní krok přidává průběžnou inline validaci i helper text, proč je kontakt potřeba.
- Souhrn umožňuje upravit službu, termín i kontakt přímo z pravého panelu bez vracení přes předchozí kroky.
- Na mobilu je booking doplněný o sticky CTA lištu, která podle stavu výběru vede na další akci nebo rovnou na odeslání.
- Po úspěšném odeslání se zobrazí samostatný confirmation flow místo jednoho souhrnného cardu:
- status blok
Rezervace přijata - jasný stav
Čeká na finální potvrzenía věta, že termín je předběžně rezervovaný - hlavní detail je kompaktní a drží skladbu
služba+datum · čas; čas zůstává dobře čitelný (např.09:30 – 10:30) - stručný blok
Co bude následovat - uklidňující věta, že termín je nyní rezervovaný a klientka nemusí dělat další kroky
- samostatný kontakt na studio až pod hlavními informacemi (desktop může být v jedné řádce
e-mail · telefon, mobil jako dvě samostatné akce) - referenční kód se nezobrazuje, dokud pro něj projekt nemá samostatné business pole používané v komunikaci
- nad confirmation panelem se nezobrazuje intro z aktivního výběru termínu (
Vyberte si termín...) - post-submit screen záměrně nezobrazuje akce
Změnit termínaniZrušit rezervaci; změny a storno patří do e-mailu nebo detailu rezervace, ne do uzavření flow - potvrzovací stránka je záměrně hustší než dřív (nižší hero, menší vertikální mezery, nižší karty), aby nepůsobila jako dlouhá landing page
- status blok
- Provozní e-mail o nové rezervaci teď obsahuje tři akce:
Potvrdit rezervaciPřesunout termínZrušit rezervaciOtevřít v administraci
- Provozní e-mail je určený pro rychlé mobilní rozhodnutí: nahoře je služba, datum, čas, klientka, e-mail a telefon jen pokud existuje; dlouhé vysvětlení akčních odkazů, technické údaje a duplicitní patička se v této šabloně nezobrazují.
- Emailové approve/reject odkazy neprovedou změnu hned po otevření; vždy nejdřív zobrazí kontrolní obrazovku s přehledem rezervace a až následně potvrzovací CTA.
- Po potvrzení rezervace systém automaticky založí návazný klientský e-mail s výsledkem rezervace a přiloženou
.icsudálostí pro osobní kalendář klientky. - Booking e-maily mají jednotný klidný design: nahoře
PP Studio, jasný headline, krátký úvod, karta se službou / datem / časem, místo, relevantní akce, jednorázový kontakt a decentní patička. Čas se zobrazuje jako09:30 – 10:30. - Potvrzovací e-mail po finálním schválení rezervace je záměrně krátký a praktický: potvrzuje rezervaci, ukazuje službu, termín a místo
PP Studio, Sadová 2, 760 01 Zlín, připomíná přiloženou kalendářovou událost a dole nenápadně nabízí změnu nebo zrušení, pokud jsou v payloadu dostupné bezpečné odkazy. - Reminder 24 hodin před termínem neobsahuje samostatné CTA
Ozvat se studiu; kontakt je jednou jakoNapište nám: info@ppstudio.czaZavolejte: +420 732 856 036. - Pending confirmation screen kalendář záměrně nenabízí;
.icspříloha patří až k e-mailu po přechodu rezervace doCONFIRMED. - Rezervační stránka je renderovaná dynamicky při requestu, takže nově publikované nebo obsazené sloty jsou vidět bez dalšího buildu.
- Hero, sekce
O mněa základní service copy jsou přepsané do klidnějšího a osobnějšího tónu; u/o-mnese vyhýbej defenzivním formulacím o praxi, agresivním slibům a superlativům typudokonalý,špičkovýnebookamžité výsledky. - Další jemné úpravy veřejné copy dělej centrálně v obsahové vrstvě
src/content/public-site.tsnebo v DB copy mapě služeb, ne přímo v route souborech. - Veřejný footer je záměrně klidný informační blok, ne marketingová patka:
- na desktopu používá kompaktní 3sloupcové rozložení
brand -> navigace + informace -> kontakt - na mobilu se skládá pod sebe v pořadí
brand -> navigace -> informace -> kontakt - navigace a právní odkazy jsou oddělené do dvou samostatně nadepsaných skupin, ne do jednoho dlouhého seznamu
- kontakt má vlastní opticky silnější blok s adresou a klikacími odkazy
tel:amailto:; telefon i e-mail se zobrazují přímo a bez textové obfuscace - spodní mikrořádek drží jen copyright a nemá přebírat roli další navigace
- v
SiteShellvariantěbookingje footer záměrně ještě kompaktnější (menší paddingy/gapy), ale obsah a odkazy zůstávají stejné
- na desktopu používá kompaktní 3sloupcové rozložení
- Stránka
/gdpruž není placeholder kostra; používá právní informační skladbuhero s kontaktem správce -> obsahová navigace -> tematické sekce. - Stránka
/obchodni-podminkyuž není pracovní návrh; používá finální právní strukturuhero s kontaktním blokem poskytovatele -> obsahová navigace -> kompaktní sekce pro rezervace, storno, cenu, průběh služby, odpovědnost, reklamace, poukazy a závěrečná ustanovení. - GDPR sekce v
src/content/public-site.tsteď počítají s jemně bohatším modelem (id, odstavce, seznamové body, volitelná poznámka), aby šla stránka rozšířit bez přepisování layoutu. - Právní sekce v
src/content/public-site.tsnově umí i volitelnýeyebrow, takže dlouhé právní stránky drží čitelnou číslovanou hierarchii bez zavádění nové specializované page komponenty. - Veřejné e-mailové odkazy jsou jemně obfuskované:
- výchozí text se zobrazuje v běžném čitelném tvaru s
@; textový zápislokalni-cast [at] domenapoužívej jen tam, kde je to vědomě zvolený copy pattern - skutečný
mailto:odkaz se skládá až na klientu po načtení stránky - pro návštěvnici zůstává chování stejné, ale jednoduché scrapery nevidí čistý e-mail přímo v HTML
- výchozí text se zobrazuje v běžném čitelném tvaru s
- Stránka
/kontaktmá nově silnější orientaci na rychlou akci:- hero drží text + CTA vlevo a vyhrazený placeholder prostor pro budoucí fotografii vpravo
- spodní část kontaktu kombinuje kompaktní mapový náhled, quick contact blok s telefonem, e-mailem, Instagramem a údajem o provozovateli a pod nimi samostatný full-width blok
Parkovánís 4 rychlými tipy (Hradská,Gahurova,Sadová,Kongresové centrum Zlín) - parkovací blok je záměrně krátký a pomáhá rozhodnout hlavně pro běžnou návštěvu cca 90-120 minut: nejlevněji, kompromis cena/vzdálenost, nejblíže nebo kryté parkování
- spodní CTA blok rozlišuje dvě cesty rozhodnutí (rovnou rezervace vs. nejdřív kontakt)
- na mobilu je dole sticky CTA lišta s rychlou rezervací, voláním a e-mail kontaktem
- Stránka
/o-mneje nově poskládaná jako scan-friendly landing page:- výraznější hero s dvěma CTA a badge služeb
- stručná sekce „Proč klientky volí PP Studio“ s klidným podnadpisem a třemi konkrétními benefity
- civilnější příběh, samostatný blok přístupu a klidná sekce o kosmetice FOR LIFE & MADAGA
- samostatná mřížka certifikací, která funguje i bez finálních admin dat díky placeholder kartám
- Následný polish pass nad
/o-mneuž nemění strukturu; upravuje hlavně proporce hero, vnitřní spacing karet, rytmus sekcí a jemnou textovou hierarchii. - Finální UI polish ještě mírně navýšil váhu textového hero sloupce, sjednotil benefit boxy do stabilnější výšky a přidal jemné hover stavy pro benefit karty a certifikace.
- Další doladění stránky
O mněmá už být jen přes drobné utility změny, ne přes nové bloky nebo přepis IA. - Texty a struktura stránky
O mnějsou centralizované vaboutContent; layout počítá s polemwhyChooseMevčetně volitelného podnadpisu sekce, popisu benefit karet, hero badge, CTA kartou i pozdějším napojením certifikací na admin data bez dalšího přepisu sekcí. - Homepage copy teď vědomě navazuje na konverzně funkční strukturu starého webu (
služba + lokalita, rychlé CTA na rezervaci/ceník, sekce pro nejistý výběr služby), ale běží na současném komponentovém základu. - Homepage hero podporuje i vizuální brand prvky přes obsahový config (
logoImage,portraitImagevsrc/content/public-site.ts); lokální assety jsou vpublic/brand/. - Browserové a PWA ikony držíme odděleně od homepage brand assetů: favicon sada žije v
src/app/favicon.ico,src/app/apple-icon.pngapublic/android-chrome-*/public/apple-touch-icon*.png. - Homepage hero lze obsahově ladit blíž původnímu webu přes
homepageContent(benefits,ctaNote) bez zásahu do routy. - Hero na homepage je záměrně klidnější: portrét je menší a pravý sloupec nepoužívá doprovodné mini boxy.
- CTA na rezervaci je dostupné v hlavičce, hero sekcích i obsahových blocích.
- Stránka
/studiopředstavuje prostředí PP Studia před první návštěvou:- navigace používá název
Studio - hero má nadpis
Klidné místo pro vaši péči, dvě CTARezervovat termínaKontakta jako úvodní vizuál bere první publikovanou fotku studia - galerie používá až další publikované fotky po hero (maximálně 6 kusů), takže se úvodní fotka v galerii neduplikuje; layout se přizpůsobuje i při 1-6 galerijních fotkách
- veřejný read model vrací jen publikované
MediaType.SALON_PHOTOassety, u kterých existuje reálný soubor ve storage - obsahové fotky studia používají
MediaAsset.altText; při chybějícím alt textu se vypíše fallbackFotografie prostoru PP Studio - pokud DB obsahuje publikovaný záznam bez fyzického souboru, asset se do
/studionezařadí a nevzniká broken image box - ve
developmentrežimu je povolený fallback na lokální obrázkypublic/dev/studio/*; v produkci se fallback nikdy nepoužívá - navazující bloky krátce popisují atmosféru, adresu a finální cestu k rezervaci nebo kontaktu
- navigace používá název
- Veřejné napojení modulu
Médiaje nyní centrální:/o-mnenačítá certifikáty přesMediaType.CERTIFICATEa hero portrét přesMediaType.PORTRAIT_ABOUT- homepage používá hero portrét přes
MediaType.PORTRAIT_HOME; pokud chybí, zůstává verzovaný brand asset /studiopoužívá publikovanéMediaType.SALON_PHOTO; první dostupná fotka je hero a následující dostupné fotky tvoří galerii/kontaktpoužívá pouze publikovanéMediaType.CONTACT_PHOTO; pokud není nahrané, zobrazí placeholder bez fotky studia
- Certifikáty, fotky prostor, reference a další budoucí obsahové obrázky mají sdílený základ přes
MediaAsseta lokální upload storage. - V admin modulu
Médiapoužívej pro fotky studia tabProstory; upload formulář v tomto tabu předvybere typSALON_PHOTOa polePořadíurčuje pořadí hero/galerie na/studio. - Pro samostatnou fotku na kontaktní stránce používej tab
Kontakt; upload formulář v tomto tabu předvybere typCONTACT_PHOTOa nejnižšíPořadíurčuje aktivní hero fotku.
- Admin login je dostupný na
/admin/prihlaseni. - Přihlašovací obrazovka používá krátké netechnické copy, neutrální e-mailový placeholder a viditelný focus stav pro klávesnicové ovládání.
- Login route
POST /api/auth/loginmá nově server-side rate limit (okno 10 minut) nad hashovanou IP a hashovaným e-mailem, aby omezila brute-force pokusy. - Při překročení limitu se login ukončí bezpečným přesměrováním na
/admin/prihlaseni?error=rate_limitedbez založení session. - Databázové účty vytvořené přes owner sekci
Přístupyse přihlašují vlastním heslem nastaveným přes pozvánku. - Recovery nikdy neotevírá webový bootstrap účet. Pro vytvoření nebo obnovu DB OWNERa použij offline příkaz
npm run admin:recover-owner -- --email owner@example.com --name 'Jméno' --confirm < heslo.txt; příkaz vyžaduje heslo alespoň 12 znaků, obnoví aktivní DB účet, revokuje otevřené pozvánky a zapíše auditADMIN_RECOVERY_OWNER_RESTOREDbez hesla. - Posledního aktivního OWNERa nelze v UI ani server action deaktivovat či degradovat. Vlastní degradace je možná až tehdy, když zůstává další aktivní OWNER.
- Session je ukládaná do
httpOnlycookie a podepisovaná pomocíADMIN_SESSION_SECRET. - Životnost admin session cookie
ppstudio-admin-sessionje 14 dní (stejná expirace je i v podepsaném JWT tokenu). - Při běžném provozu adminu se session průběžně prodlužuje (sliding refresh): pokud při admin requestu zbývá do expiry méně než 48 hodin,
proxyvystaví novou cookie. - Bezpečnostní strop je 45 dní od prvního přihlášení; po překročení je vyžadované nové přihlášení.
- Časování session lze upravit env proměnnými
ADMIN_SESSION_IDLE_MAX_AGE_SECONDS,ADMIN_SESSION_REFRESH_WINDOW_SECONDSaADMIN_SESSION_ABSOLUTE_MAX_AGE_SECONDS. - Po přihlášení aplikace přesměruje uživatele na domovskou admin cestu podle role:
OWNER->/adminSALON->/admin/provoz
- Sekce dostupné pro obě role:
- Přehled
- Rezervace
- Volné termíny
- Klienti
- Mobilní admin navigace používá vlastní drawer; při jeho otevření se horní lišta dočasně schová, aby se menu nepřekrývalo s vlastním obsahem.
- Owner sekce
Nastavenínově obsahuje i blokKalendář:- zapnutí feedu
- zkopírování subscription URL
- rotaci tokenu
- vypnutí feedu
- stručný návod pro Apple Kalendář / iCloud subscription
- ICS feed je určený jen jako přehled pro majitelku:
- ukazuje pouze rezervace ve stavu
CONFIRMED - neumožňuje editaci ani obousměrnou synchronizaci
- po rotaci nebo vypnutí starý odkaz přestane fungovat
- ukazuje pouze rezervace ve stavu
- Sekce
Přehledna/admina/admin/provozje nyní operativní dashboard dne:- layout je rozdělený na hlavní pracovní plochu a pravý sidebar; levý navigační sidebar zůstává součástí shellu
- nahoře je sjednocený blok
Provozní přehled, který v jednom cardu spojuje datum, dominantní počet dnešních rezervací, další klientku a hlavní CTAVytvořit rezervaci / Dnešní plán / Dostupnost - pokud existují čekající potvrzení, dashboard je ukáže jako výrazný akční alert nad dnešním plánem; bez pending stavu zůstávají alerty menší a sekundární
Dnešní plánje hlavní pracovní sekce: používá kompaktní server-rendered seznam rezervací s výrazným časem vlevo, stavovým badge a přímým proklikem na detail rezervace- pokud má dnešní rezervace poznámku, plán ji ukáže přímo u klientky s původem
KlientkaneboInterně; rezervace bez poznámek zůstávají bez doplňkového řádku - pravý sidebar je podpůrný: obsahuje
Rychlé akce,Tento týdenaVýkon webu; primární CTA pro ruční rezervaci zůstává nahoře v hero liště - KPI strip už neopakuje všeobecný reporting; drží jen provozní metriky
Dnes rezervace,Volná okna dnes,Týdenní obsazenostaVolné sloty tento týden - na mobilu dashboard přepíná do jednoho svislého proudu: hero, alerty, KPI, dnešní plán, volné termíny a až potom pravý podpůrný sloupec
- overview používá server-rendered Suspense fallback se skeletonem, takže při načítání nepůsobí jako prázdná stránka
- Sekce
Rezervaceje nyní přepracovaná jako kompaktní pracovní seznam na/admin/rezervacea/admin/provoz/rezervace:- místo vysokých karet používá hustý řádkový grid se sloupci
Rezervace,Čas,Status,Zdroj,Kontakt,Akce - každá rezervace drží klientku + službu a datum + čas ve dvou krátkých řádcích bez zbytečné výšky
- na mobilu se každá položka skládá do dvousloupcové karty s názvem přes celou šířku, přehlednými metadaty a plnošířkovým footerem pro akce
- horní statistiky jsou zmenšené do jedné souhrnné řady místo velkých karet
- hlavička seznamu zůstává sticky při scrollu, takže jsou sloupce stále čitelné
- přímo v řádku jsou rychlé akce
Potvrdit,ZrušitaOtevřít; na menších šířkách fungují jako plný footer pod řádkem a odlgvýše mají úsporný vlastní sloupec s kompaktnější kapslí - sloupec
Statusje centrovaný jako samostatný grid item, aby badge seděly přesně pod hlavičku Zrušenárezervace má ve sloupciStatusjen lehce červený tón pro rychlé rozpoznání- stav se zobrazuje přes barevné badge, aby bylo na první pohled vidět, co čeká, co je hotové a co je zrušené
- sloupec
Zdrojkombinuje provozní původ rezervace (Web,Telefon, ...) a akviziční zdroj (Google,Facebook,Instagram,Firmy.cz/Seznam,Direct,Other), pokud je dostupný - toolbar nově obsahuje CTA
Přidat rezervaci, které proOWNERiSALONotevírá pravý drawer pro plnohodnotné ruční vytvoření rezervace - ruční rezervace stále vzniká jako běžný
Booking; používá stejnou doménovou create logiku jako veřejný booking a ukládá jen doplňková metadatasource,isManual,manualOverride,createdByUserId - drawer umí vyhledat nebo propojit existující klientku podle jména, telefonu i e-mailu, případně rovnou založit novou
- termín lze založit buď ze slotů respektujících veřejnou dostupnost, nebo ručně přes datum a čas; pokud ruční čas neleží ve veřejné dostupnosti, systém upozorní na interní výjimku a uloží ji auditovaně
- sticky footer nabízí
Vytvořit rezervaciaVytvořit a poslat potvrzení; při odeslání e-mailu se používá stejné email/log/ICS flow jako u běžných rezervací
- místo vysokých karet používá hustý řádkový grid se sloupci
- Sekce
Klientije nyní produkčně použitelná pro obě role na/admin/klienti,/admin/provoz/klientia v detailu na/admin/klienti/[clientId],/admin/provoz/klienti/[clientId]:- seznam podporuje hledání přes jméno, e-mail, telefon i interní poznámku
- filtry umí omezit aktivní/neaktivní profily a přepnout řazení podle poslední návštěvy, počtu rezervací, jména nebo vytvoření
- rychlé filtry přepínají provozní řezy
Vše,S rezervací,Bez rezervace,Bez kontaktu,S poznámkouaNové za 30 dní - hlavní přehled klientů je záměrně kompaktní: horní statistiky ukazují celkový počet, nové profily za 30 dní, profily bez kontaktu a profily s poznámkou v nízkém stripu
- desktopový seznam je tabulkový CRM přehled se sloupci
Klientka,Kontakt,Rezervace,Poslední návštěva,Poznámka,Stav,Akce; na mobilu se skládá do kompaktních karet - chybějící kontakt rozlišuje
bez e-mailu,bez telefonuabez kontaktu; dlouhé e-maily i technické názvy jsou zkrácené přes ellipsis - testovací profily podle bezpečných signálů (
example.com, voucher/collision názvy nebo e-maily) se pouze jemně označí badgetest, nikdy nemažou - detail klientky ukazuje kontakty, poslední a budoucí termín, nejčastější službu a posledních 10 rezervací
- interní poznámka se upravuje přímo v detailu klientky a po uložení se propisuje do obou admin oblastí
- Sekce
Přístupyje nyní vyhrazená jen proOWNERna/admin/uzivatele:- obrazovka je rozdělená na hlavní blok
Seznam přístupůa vedlejší read-only blokPřehled rolí - systém používá pouze dvě role
OWNERaSALON; neexistuje žádná roleADMIN - každý přístup ukazuje jméno, e-mail, roli, stav účtu, krátký helper text a dostupné akce
- stavy účtu jsou
Aktivní,Pozvánka čeká,DeaktivovanýaSystémový účet - systémové přístupy z env se v UI zobrazují pouze jako
Systémový účeta zůstávají read-only bez technických detailů - owner může u databázových účtů založit pozvánku, upravit jméno a e-mail, přepnout roli, deaktivovat nebo znovu aktivovat účet a otevřít detail
- akce
Pozvat uživateleiZnovu poslat pozvánkuodesílají reálný e-mail přes SMTP vrstvu - pozvánka vede na route
/admin/pozvanka/[token], kde si uživatel nastaví heslo a dokončí aktivaci přístupu - pokud SMTP dočasně selže, přístup se i tak uloží a UI zobrazí, že e-mail nebylo možné doručit
- obrazovka je rozdělená na hlavní blok
- Lokální filesystem adapter je v
src/lib/media/*. - Sdílená feature service pro budoucí owner/salon upload workflow je v
src/features/media/lib/media-library.ts. - Metadata se ukládají do tabulky
MediaAsset, zatímco binární soubor zůstává na filesystemu. - Podporované typy jsou aktuálně obrázky
jpg,jpeg,png,webp. - Maximální velikost souboru je 8 MB.
- Každý upload dostane krátký generovaný identifikátor a ukládá se jako
{id}-original.<ext>,{id}-optimized.<ext>a{id}-thumbnail.<ext>bez použití původního názvu souboru. - Relativní storage path má tvar
certificates/2026/04/<id>-original.<ext>; běžné veřejné typy používají kořenyspaces/,contact/,portraits-home/neboportraits-about/. - Publikace je řízená jen přes
MediaAsset.isPublished; nové uploady se nepřesouvají mezipublic/private. - Modul
Médiamá první produkční napojení:- admin upload, editaci a mazání přes
/admin/mediaa/admin/provoz/media - běžné admin typy
CERTIFICATE,SALON_PHOTO,CONTACT_PHOTO,PORTRAIT_HOMEaPORTRAIT_ABOUT; legacy/nepoužívané typyPORTRAITaGENERALzůstávají jen v DB schématu kvůli kompatibilitě - admin UI je záměrně kompaktní pracovní nástroj: krátký header, 4 rychlé statistiky, upload panel s dropzónou, tabs s počty a hustší grid karet
- každá karta média ukazuje náhled, titulek nebo soubor, typ, publish stav, rozměry, velikost a zřetelné
Použití+Sekce
- admin upload, editaci a mazání přes
- V nastavení je blok
Voucherys výchozím vzhledem (aktuálněKlasický) a výchozí platností 1–60 měsíců. Logo ani PDF template se vMédiíchnevybírají; grafika je pevnou součástí verzovaného master PDF. LegacySiteSettings.voucherPdfLogoMediaIdzůstává v databázi kvůli kompatibilitě, ale aktuální renderer ho nepoužívá.- publish/unpublish jde přímo z karty bez otevírání editace; detailní změny zůstávají v kompaktním dialogu
- prázdná knihovna i prázdné filtry mají vlastní CTA zpět na upload panel
- veřejné zobrazení certifikátů v sekci
Certifikacena stránce/o-mne - veřejné zobrazení publikovaných fotek studia na stránce
/studio - backend napojený na
createMedia(),listMedia(),updateMedia()adeleteMedia() - stránka
/o-mnebere pouzeMediaType.CERTIFICATEaisPublished = true - stránka
/studiobere pouzeMediaType.SALON_PHOTOaisPublished = true
- Zálohuj databázi i upload root; jedna bez druhé nestačí pro úplnou obnovu médií.
- Při deployi se upload root nemaže ani nepřegenerovává, protože není součástí build artefaktů.
- Pokud upload začne selhávat, první kontrola má být:
- existence cesty z
MEDIA_STORAGE_ROOT - práva procesu k zápisu
- dostupnost veřejné URL
/media/public/*nebo legacy/media/* - Služby
- Kategorie služeb
- existence cesty z
- Sekce
Službyje nyní provozně použitelná pro obě role na/admin/sluzbya/admin/provoz/sluzby:- seznam nově funguje jako rychlá pracovní plocha: fulltext, filtr stavu, veřejné rezervace, kategorie a řazení
- v kartách jsou rychlé akce
aktivovat/deaktivovat,veřejná/interní,duplikovata jednoduché posuny v pořadí - každá karta ukazuje provozní kontext, stavové badge a upozornění na problematické stavy
- formulář podporuje
UložitiUložit a zavříta novou službu lze založit přes jasné CTANová služba - při přepnutí mezi službami se detail vždy přenačte podle skutečně vybrané položky (nepřebírá hodnoty z předchozí karty)
- v detailu služby lze ručně zapnout zobrazení na homepage a nastavit pořadí v sekci
Doporučené služby - v detailu služby lze nastavit
Čas na úklid po službě; hodnota se ukládá jako interní metadata služby, klientce se nezobrazuje jako délka služby a při rezervacích se používá pro interní blokaci po skončení služby - v detailu služby je jediný obsahový blok
Veřejná prezentace; poleVeřejný úvodje zdrojem textu pro web i rezervační krok výběru služby, takže se stejný text neudržuje duplicitně - detail se otevírá jako pravý overlay drawer (desktop i mobil), takže seznam zůstává viditelný v pozadí a obsluha neztrácí kontext
- skutečná změna ceny v detailu služby zapisuje audit do
ServicePriceChangeLog, takže lze dohledat původní i novou cenu, čas a admin aktéra - detail služby zároveň ukazuje sekci
Historie cenys posledními auditními změnami, takže není nutné kvůli běžnému dohledání chodit přímo do databáze - veřejný booking flow bere službu jen pokud je
isActive = true,isPubliclyBookable = truea její kategorie je aktivní
- Sekce
Kategorie služebje nyní produkčně použitelná pro obě role na/admin/kategorie-sluzeba/admin/provoz/kategorie-sluzeb:- horní přehled používá kompaktní souhrnnou lištu místo vysokých stat karet
- seznam kategorií je hustší a víc provozně orientovaný: název, pořadí, kontext služeb, stav badge, toggle a akce jsou na jednom řádku
- detail se otevírá jako pravý overlay drawer (desktop i mobil), takže je možné rychle procházet kategorie bez skákání mezi stránkami
- nahoře jsou 4 stat karty (
Aktivní,Kategorie se službami,Prázdné,Potřebují pozornost) a filtry s chipyPrázdné,Bez veřejné služby,S upozorněním - seznam ukazuje název, pořadí, aktivitu, počet všech služeb i kontext aktivních a veřejných služeb
- problémové kategorie mají zvýrazněný warning stav a jemně odlišený border
- přímo v seznamu jsou rychlé akce
aktivovat/deaktivovat,otevřít detail,zobrazit službya posuny v pořadí - přepnutí aktivního stavu a posun v pořadí probíhá okamžitě optimistic UI přes server action bez reloadu
- editor umožňuje upravit název, volitelný popis, pořadí a aktivní stav; kategorie už nemá samostatný
Veřejný název, web i ceník vždy používajíNázev kategorie - detail dál nabízí CTA
Vytvořit službuaOtevřít služby této kategorie - novou kategorii lze založit přes jasné CTA
+ Nová kategorie; editace i create běží ve stejném pravém drawer flow - mazání je povolené jen pro prázdné kategorie bez služeb; jinak je doporučené kategorii pouze vypnout
- změna pořadí nebo aktivity se promítá do adminu, veřejných výpisů
/sluzbya/ceniki do veřejného booking flow
- Sekce jen pro
OWNER:- Přístupy
- Email logy
- Nastavení
- Sekce
Nastaveníje nyní produkčně použitelná proOWNERna/admin/nastaveni:- blok
Salonspravuje název salonu, adresu, telefon, kontaktní e-mail a Instagram - blok
Rezervacedrží jen skutečně globální booking pravidla: minimální předstih, horizont dopředu a storno limit pro self-service storno - blok
E-maily a notifikacespravuje admin notifikační e-mail, sender name, sender email a krátkou patičku potvrzovacích e-mailů - formuláře mají jednotný footer se stavem ukládání, kratší popisky a mobilní rozložení, aby šly snadno používat i na telefonu
- nahoře je malý orientační blok, který rychle vysvětlí, co do které části patří
- technické SMTP údaje, app URL a session secret zůstávají správně mimo admin v env
- blok
- Tón admin obrazovek je záměrně jednotný: klidný, krátký a ne-technický, aby se v něm obsluha zbytečně neztrácela.
- Lite provozní menu je záměrně kratší a drží jen to, co recepce a tým potřebují nejčastěji:
- Přehled
- Dnešní rezervace
- Termíny
- Klientky
- Nabídka
- Kategorie služeb
- Levý sidebar zůstává i po redesignu dashboardu záměrně úzký; když budeš upravovat shell spacing, priorita je ponechat co nejvíc šířky pro operativní obsah, ne pro dekorativní chrome.
- Detail rezervace je nyní dostupný jak pro
OWNER, tak proSALON:OWNERna/admin/rezervace/[bookingId]SALONna/admin/provoz/rezervace/[bookingId]- nahoře používá kompaktní statickou hlavičku s návratem do seznamu, termínem, badge stavu, zdrojem a rychlými kontaktními akcemi
- pod hlavičkou je jeden kompaktní souhrn místo více podobných boxů se stejnými daty
- hlavní blok
Akce s rezervacídrží dostupné změny stavu a krátký stavový kontext bez dlouhých odstavců - poznámky jsou rozdělené na klientskou a interní; interní poznámku lze uložit samostatně i bez změny statusu
- historie změn zůstává dole jako hustší časová osa se stavem, aktérem, časem a dostupným zdrojem změny
- Správa slotů je nyní produkčně použitelná pro obě role:
- jediný FullCalendar planner na
/admin/volne-terminya/admin/provoz/volne-terminy - route
detailaupravitvrací zpět do planneru ve správném týdnu; samostatná routenovyuž neexistuje
- jediný FullCalendar planner na
- Slot workflow podporuje:
- plánování po týdnech ve FullCalendar mřížce po 30 minutách v pracovním okně
06:00-20:00 - kliknutím přidání či odebrání dostupnosti a tažením úpravu rozsahu přímo v mřížce
- na mobilu pohledy Den, Po–Pá a Víkend bez druhé plannerové implementace
- automatické sloučení sousedních půlhodin do souvislých intervalů
AvailabilitySlot - zobrazení rezervací, omezených intervalů, neaktivních slotů a minulého času
- server-side ochranu proti zásahu do rezervací, omezených slotů a překryvům
- plánování po týdnech ve FullCalendar mřížce po 30 minutách v pracovním okně
- K datu
19. dubna 2026je sekce/admin/volne-terminy*a/admin/provoz/volne-terminy*znovu aktivní jako týdenní planner. - Hlavní práce probíhá jen přes týdenní kalendář; samostatný formulář pro běžnou úpravu dostupnosti už není potřeba.
- Aktuální podoba adminu dává prioritu mřížce týdne; levý sidebar je užší a mobil používá drawer pro navigaci.
- 30min mřížka slouží jen jako editace v admin UI. Do databáze se ukládají souvislé intervaly
startsAt-endsAt, aby zůstala kompatibilita s veřejným booking flow i delšími službami. - FullCalendar ukládá každou změnu dostupnosti průběžně; při chybě lze zápis zopakovat nebo obnovit poslední uložený stav.
- Planner přímo neupravuje sloty, které už obsahují rezervace, omezení služeb, poznámky nebo jinou kapacitu než
1; takové intervaly jsou v kalendáři vidět jako omezené a zůstávají chráněné. - Za „obsahují rezervace“ se pro rychlou planner editaci počítají hlavně aktivní nebo provozně relevantní vazby.
CANCELLEDhistorie se při změně dostupnosti přesouvá do archivovaného slotu na pozadí, takže obsluha v mřížce nevidí zbytečný blok jen kvůli storno minulosti. - Výchozí týden v planneru je počítaný nad lokálním datem
Europe/Prague, takže týden vždy začíná pondělím i kolem časových posunů. - Autor změny dostupnosti je vždy aktivní databázový
AdminUser; nouzový webový bootstrap účet neexistuje. - Z detailu rezervace lze bezpečně změnit stav pouze v povolených krocích:
PENDING -> CONFIRMEDCONFIRMED -> COMPLETEDPENDING/CONFIRMED -> CANCELLEDCONFIRMED -> NO_SHOW
- Akce
CONFIRMED -> COMPLETEDje dostupná až po skončení naplánovaného termínu (scheduledEndsAt); budoucí potvrzená rezervace proto nemůže omylem přestat blokovat kapacitu a objevit se v dashboardu jako volné okno. - Dnešní dashboard timeline zobrazuje i dokončené dnešní rezervace jako tlumené
Hotovo; volná okna se počítají jen od aktuálního času dopředu, takže minulý úsek po hotové službě nevypadá jako nově dostupný termín. - Sekce
Volné termínyv denním planneru zobrazuje také dokončené rezervace jako tlumené cyanHotovo, takže historicky obsazený čas zůstává čitelný místo anonymního technického omezení a nesplývá se zelenou dostupností. - Inspektor výběru v gridu u hotové rezervace používá historický text a odkazuje obsluhu na detail rezervace místo obecné hlášky o rezervovaném čase.
- Volba akce v bloku
Změna stavuje řešená přes klikací karty místo selectu; aktivní karta je barevně zvýrazněná podle typu akce (potvrzení zeleně, zrušení červeně), aby obsluha hned viděla, co je vybrané. - Blok
Změna stavunově předvybírá nejčastější další krok a pod výběrem ukazuje krátké shrnutí dopadu akce, takže je menší riziko chybného uložení ve spěchu. - Každá změna stavu z detailu zapisuje položku do
BookingStatusHistoryvčetně admin aktéra, důvodu a poznámky. - Aby se owner sekce
Email logyneopírala o ručně zastaralý Prisma klient,npm run devinpm run buildsi nyní předem samy spouštějíprisma generate. - Ochrana není řešená jen skrytím položek v menu:
proxy.tsna/admin/*validuje podpis i expiraci session JWT cookie (ne jen přítomnost); neplatnou cookie smaže a přesměruje na login- server-side guard helpery kontrolují oprávnění každé admin route
- nedovolený vstup se přesměruje na domovskou admin stránku role nebo skončí
notFoundpro neplatnou sekci
- Owner a salon route soubory nyní používají sdílené factory wrappery (
src/features/admin/lib/admin-route-factories.tsx), takže URL i oprávnění zůstávají stejné, ale logika není duplikovaná. - Admin shell byl vizuálně zpevněný pro provozní použití:
- širší sidebar na desktopu a sticky navigace při scrollu
- hlavní obsah má ochranu proti horizontálnímu přetečení (
min-w-0,overflow-x-clip) - hlavičky a metriky v admin kartách mají responzivní velikosti pro menší šířky
AvailabilitySlotje hlavní entita dostupnosti a nese časový interval, stav, kapacitu a interní/veřejné poznámky.- Admin CRUD slotů nepoužívá pevnou otevírací dobu; každý slot se zakládá ručně jako samostatný časový interval.
AvailabilitySlotmá explicitníserviceRestrictionMode, takže je zřejmé, zda slot přijímá jakoukoli službu nebo jen vybrané služby.AvailabilitySlotServiceumožňuje slot omezit jen na konkrétní služby, když jeserviceRestrictionMode = SELECTED.- Server-side slot validace navíc hlídá:
endsAt > startsAt- kapacitu minimálně
1 - kolizi s jiným aktivním slotem ještě před zápisem
- zákaz snížení kapacity pod počet aktivních rezervací
- zákaz výběru služeb, které by rozbily už navázané aktivní rezervace
- Kategorie a služby jsou samostatné DB entity, které se dnes plní přes import nebo admin správu, ne přes hardcoded seed.
Service.isPubliclyBookableodděluje interně aktivní službu od služby skutečně nabízené ve veřejné rezervaci.Service.cleanupMinutesdrží volitelný interní čas na úklid po službě s defaultem0; při vytváření/přesunu rezervace se snapshotuje doBooking.cleanupMinutesa zaokrouhlený blok doBooking.cleanupBlockMinutes.Booking.blockedUntildrží interní konec blokace (scheduledEndsAt + cleanupBlockMinutes), zatímco klientský termín zůstáváscheduledStartsAt -> scheduledEndsAt.Bookingdrží snapshot klienta, služby i času, takže pozdější změny ceníku nebo názvů služeb nepoškodí historická data.- Voucher zadaný u rezervace je jen záměr uložený na
Booking.intendedVoucherIda snapshot polí; skutečné uplatnění voucheru smí vzniknout až v rámci dokončení návštěvy, kdy se atomicky vytvoříVoucherRedemptiona rezervace přejde doCOMPLETED. - Samostatné veřejné ověření voucheru přes
/vouchery/overenismí pouze číst voucher přes bezpečný serverový helper a nesmí měnit žádná voucherová ani booking data. Bookingdrží metadata posledního přesunu (rescheduledAt,rescheduleCount) a reminder queue stav (reminder24hQueuedAt,reminder24hSentAt); historický self-relation chain zůstává jen jako legacy pole a nové reschedule flow ho nepoužívá.BookingRescheduleLogje samostatná auditní tabulka pro přesuny termínu s původním a novým intervalem, aktérem a volitelným důvodem změny.Booking.reminder24hSentAtdrží informaci, že klientský 24h reminder už byl úspěšně uzavřený;Booking.reminder24hQueuedAtzase brání duplicitnímu enqueue stejného reminderu pro aktuální termín.Bookingnově ukládá i akviziční metadata (acquisitionSource,acquisitionReferrerHost,acquisitionUtmSource,acquisitionUtmMedium,acquisitionUtmCampaign) odvozená zutm_*a referrer hostu.BookingStatusHistoryslouží jako audit změn stavu a rozlišuje akci uživatele, klienta nebo systému.ServicePriceChangeLogje samostatná auditní tabulka pro změnyService.priceFromCzk; ukládá starou a novou cenu, aktéra a čas změny.- Admin detail rezervace zobrazuje historii změn jako provozní timeline, takže salon i owner vidí, kdo a kdy stav upravil.
BookingActionTokenukládá pouze hash tokenu pro storno a přesun termínu, nikdy ne surovou hodnotu tokenu.- Klientský manage flow
/rezervace/sprava/[token]přijímá jen hashovaný token typuRESCHEDULE; bez validního tokenu neukáže žádná data rezervace. - Klientský manage flow při obyčejném načtení nevydává nový storno token. Tlačítko
Zrušit rezervacinejdřív přes server action vytvoří jednorázovýCANCELtoken a až potom přesměruje na potvrzovací storno stránku. EmailLogumožňuje trasovat odeslané i neúspěšné e-maily navázané na klienta, rezervaci a případný token.EMAIL_DELIVERY_MODE=logje jen vývojový/safe-mode režim; loguje maskovaného příjemce a anonymizovaný subject, ne plnou zákaznickou komunikaci.- Health je rozdělený na veřejnou bez-DB liveness
/api/health/live, veřejný readiness/api/healths jedinýmSELECT 1a owner-only/api/health/diagnosticss detailem workeru, fronty, incidentů a release identity. - Owner-only sekce
Email logynyní funguje jako business-first přehledEmail logy:- nahoře ukazuje health stav
OK / Warning / Errorpodle aktivních delivery incidentů, retry, pending fronty a poslední relevantní chyby - krátké metriky shrnují
Dnes odesláno,Za posledních 7 dní,Čeká na odeslání,Aktivní incidentyaPoslední odeslánív nižším KPI stripu; součty odeslaných zpráv znamenají předání providerovi, nikoli potvrzené doručení - health copy zůstává stručné; při čistém stavu používá text
Emaily fungují správněa krátké vysvětlení o prázdné frontě - hlavní sekce
Poslední emailypropojuje typ zprávy, stav, příjemce, vazbu na rezervaci, časy, pokusy a rychlé akceOtevřít rezervaci / Detail emailu / Zkusit znovu - badge typu rozlišuje
Přijetí rezervaceprobooking-confirmation-v1a finálníPotvrzení rezervaceprobooking-approved-v1 - tracking badge v přehledu e-mailů je napojený na reálné Resend webhook eventy (
email.delivered,email.opened,email.clicked,email.bounced,email.failed,email.suppressed); fallback bez eventů zůstáváTracking připraven - při chybových Resend eventech (
email.bounced,email.complained,email.failed,email.suppressed) systém naváže owner Pushover notifikaci o následném problému doručení po předání e-mailu providerovi; upozornění se posílá jen při prvním zachycení konkrétního chybového stavu - Resend produkční setup:
- v produkčním
.envnastavteEMAIL_DELIVERY_MODE=background,EMAIL_TRANSPORT=resend,RESEND_API_KEYaRESEND_WEBHOOK_SECRET - po změně schématu nasaďte migrace (
npx prisma migrate deploy), abyEmailLogobsahoval tracking sloupce - v Resend dashboardu nastavte webhook endpoint
POST https://<produkční-doména>/api/webhooks/resend - do webhooku zapněte email eventy minimálně
sent,delivered,delivery_delayed,opened,clicked,bounced,complained,failed,suppressed - signing secret z Resend webhooku uložte do
RESEND_WEBHOOK_SECRET - po deploy restartujte
ppstudio-webappstudio-email-worker - ověřte v
/admin/email-logy, že nové záznamy mají vyplněné tracking stavy (Doručeno,Doručeno - otevřeno,Nedoručeno - odmítnuto serverem (bounce)apod.)
- v produkčním
Další pokusse v hlavním seznamu ukazuje jen u stavůČekáaRetry- původní pending/retry/error fronty zůstávají níž v debug bloku
Technický stav fronty, který je defaultně sbalený do kompaktního souhrnu
- nahoře ukazuje health stav
- Detail konkrétního e-mailu na
/admin/email-logy/[emailLogId]je nově business-first:- používá stejný jediný levý navigační sloupec jako přehled
Email logy; navigace se v detailu nesmí duplikovat - nahoře ukazuje kompaktnější header s názvem emailu, jedním finálním stavem
Odesláno / Čeká / Retry / Selhalo / Nedoručeno, příjemcem, klientkou, rezervací a klíčovým časemOdesláno / Poslední pokus - hned pod headerem drží zhuštěné rychlé akce
Zpět na přehled,Otevřít rezervaci, případněZkusit znovuneboUvolnit zaseknutý jobv nízké operativní liště - pravý sloupec tvoří hustší souhrn
Typ emailu / Šablona / Příjemce / Provider / Poslední pokus / Odesláno / Počet pokusů - levý sloupec drží navázané entity
Rezervace / Klientka / Token akcejako kompaktní řádky; token je defaultně maskovaný a plně se ukáže až po kliknutí naZobrazit - payload, provider metadata a raw debug data jsou až dole v nižším rozbalovacím bloku
Technické detaily, defaultně zavřeném, s dalším rozbalenímZobrazit citlivá data - pokud existuje chyba, detail ukáže nejdřív stručný čitelný popis a až pak kompaktní rozbalitelný technický detail chyby
- používá stejný jediný levý navigační sloupec jako přehled
- Po úspěšné akci se na detailu objeví krátká potvrzovací hláška, aby bylo zřejmé, že operace proběhla.
- Veřejný booking flow po odeslání:
- veřejný web
/,/sluzby,/cenika detail služby nyní čerpá z databáze v request-time - admin změny se do něj promítnou bez rebuildů
- route
/rezervacepodporuje query parametrserviceve tvaru/rezervace?service=<slug>pro marketingové deep linky na konkrétní službu - předvýběr služby podle
serviceslug funguje jen pro službu, která je v právě načteném veřejném katalogu; neplatný, neaktivní nebo neveřejný slug se bezpečně ignoruje - globální booking pravidla čte ze
SiteSettings, ne z natvrdo zapsaných konstant - znovu validuje službu a termín server-side
- naváže nebo vytvoří klienta podle e-mailu
- vytvoří rezervaci se stavem
PENDING(čeká na schválení) a se snapshotem služby a času - k veřejné rezervaci uloží i akviziční zdroj z cookie trackeru (
ppstudio-booking-acq) - zapíše audit změny stavu
- připraví storno token a e-mailový log s informací o přijetí rezervace
- uloží e-mail jako
PENDINGv background režimu neboSENTv log režimu
- veřejný web
- Confirmation screen a klientské booking e-maily sdílejí stejnou obsahovou hierarchii:
- jasný stav rezervace
- dominantní karta
služba / datum / čas - místo
PP Studio, Sadová 2, 760 01 Zlín - akce mimo informační copy a bez dominantního storna
- kontakt až jako spodní podpůrná sekce, pouze jednou
- 24h reminder e-mail je úsporný: služba, datum, čas, místo, sekce
Potřebujete změnu?, sekundární akceZměnit termín/Zrušit rezervacia jednorázový kontakt. - Referenční kód rezervace už se v klientské komunikaci nezobrazuje; pro změnu nebo storno se používají konkrétní tokenizované odkazy a textové shrnutí služby s termínem.
- Pokud se termín mezitím obsadí, služba přestane být aktivní nebo slot přestane odpovídat délce služby, uživatel dostane konkrétnější chybu místo obecného selhání.
- Při krátkodobém DB konfliktu (
Prisma P2034, např. serializační/write konflikt) veřejné vytvoření rezervace transakci automaticky zopakuje až 5× s krátkým backoff, aby flow méně padal na náhodné souběhy. - Veřejný submit je lehce rate-limitený podle IP a e-mailu; opakované pokusy v krátkém čase skončí blokací s user-friendly hláškou.
- Krok 2 už skrývá i sloty, které jsou pro vybranou službu příliš krátké.
- Server při odeslání rezervace navíc kontroluje i zvolený
startsAt, takže klientka nemůže odeslat čas mimo hranice slotu ani čas kolidující s už existující rezervací. - Pokud rezervace obsadí jen část delšího slotu s kapacitou
1, systém slot interně rozdělí na rezervovaný úsek a samostatné volné zbytky, takže v admin planneru lze s volnými částmi dál pracovat po blocích. /rezervace/storno/[token]je produkční self-service storno stránka:- ověří hash tokenu server-side
- zobrazí bezpečný potvrzovací krok
- po potvrzení zruší rezervaci a zapíše audit, ale jen pokud rezervace ještě splňuje globální storno limit ze settings
- uloží storno potvrzení do
EmailLogpro worker nebo doSENTv log režimu
/rezervace/sprava/[token]je produkční self-service změna termínu:- ověří hash tokenu typu
RESCHEDULEserver-side - ukáže službu, aktuální datum, čas ve formátu
13:30 – 14:00a stav rezervace - nabídne jen veřejně dostupné nové časy pro stejnou službu a délku
- primárně zobrazuje nejbližší dostupné dny jako větší klikatelné chips a sekundárně kalendář se zvýrazněnými dny s dostupností
- po výběru dne zobrazí jen sloty pro tento den a po výběru času plynule posune klientku na potvrzení
- storno je až na konci stránky jako slabý textový odkaz, aby nekonkurovalo změně termínu
- online změnu zablokuje při zrušené/uzavřené rezervaci, neplatném tokenu nebo méně než
bookingCancellationHourspřed termínem - po potvrzení volá stejné
rescheduleBooking(...)jako admin detail a do historie zapisujechangedByClient = true
- ověří hash tokenu typu
- Pokud se rezervace přesune, ale klientce nepřijde e-mail o změně termínu:
- zkontrolujte v admin sekci
Email logy, jestli vznikl záznamBOOKING_RESCHEDULED - pokud log nevznikl, hledejte v server logu chybu
Booking reschedule notification enqueue failed - samotný přesun termínu i auditní historie zůstávají uložené, protože e-mail se zakládá až po úspěšném commitnutí změny rezervace
- zkontrolujte v admin sekci
- Pokud po přesunu nevzniká nový 24h reminder:
- ověřte, že booking má po přesunu
reminder24hQueuedAt = nullareminder24hSentAt = null - spusťte
npm run email:worker:once - zkontrolujte, že nový termín ještě nezačal a není více než 26 hodin od aktuálního času; scheduler umí dohnat i termín po původním okně
25h-26h
- ověřte, že booking má po přesunu
- Pokud worker selhává na chybě
Invalid input: expected string, received undefineds cestoumanageReservationUrl:- jde typicky o starší
EmailLog.payload, který polemanageReservationUrlještě neobsahuje - aktuální renderer je backward-compatible a e-mail odešle i bez tohoto pole, jen bez CTA
Změnit termín - pokud chyba přetrvává, restartujte worker a ověřte, že běží nová verze aplikace
- jde typicky o starší
- Pokud se v adminu změnila cena služby a potřebujete dohledat, kdo zásah provedl:
- ověřte, že je nasazená migrace
20260424103000_service_price_change_log_v1 - hledejte záznam v
ServicePriceChangeLogpodleserviceIda času změny - historické rezervace zůstanou konzistentní, protože
Bookingdál drží vlastníservicePriceFromCzksnapshot
- ověřte, že je nasazená migrace
- Pokud
/admin/sluzbyv devu vrací odpověď velmi pomalu a browser házíChunkLoadErrorna Turbopack chunks:- ověřte, že list view bez
serviceIdnenačítá detail služby - ověřte, že seznam služeb načítá jen sloupce potřebné pro list
- ověřte, že stavové metriky běží přes jeden agregační dotaz (
groupBy) místo více samostatnýchcount
- ověřte, že list view bez
- Pokud
/voucheryv devu po načtení nebo při opakované navigaci hlásíChunkLoadErrorneboFailed to fetch RSC payload:- ověřte, že route nenačítá celý veřejný katalog služeb jen kvůli několika doporučeným kartám
- pro voucher landing page používejte limitovaný helper
getVoucherSuggestedServices(3), ne plnégetPublicServices()
- Chyba
Route "... " used params.slug. params is a Promisev Next.js 16 znamená, že route používá starý synchronní přístup k dynamickým parametrům. - Oprava:
- v
page.tsxagenerateMetadatatypujparamsjakoPromise<{ ... }> - nejdřív proveď
const { slug } = await params(nebo odpovídající pole) - až potom parametr použij v DB dotazech nebo renderu
- v
- Referenční implementace v projektu:
src/app/(public)/sluzby/[slug]/page.tsx. - Chyba build procesu
Invalid segment configuration export detectedznamená, že některý App Router segment config export není staticky analyzovatelný. - Oprava:
- v route souboru používej pro
revalidate,dynamic,fetchCache,runtime,preferredRegion,maxDurationpřímo literály nebo jiné staticky vyhodnotitelné hodnoty - nepoužívej výrazy typu
60 * 60 * 24; použij rovnou86400 - v tomto projektu je referenční oprava v
src/app/sitemap.ts
- v route souboru používej pro
- V detailu voucheru lze v adminu upravit jen provozní údaje: jméno kupujícího, e-mail kupujícího, platnost do a interní poznámku. Kód, typ, hodnota, měna, služba, čerpání ani PDF identita se běžnou editací nemění.
- Voucher se v adminu ruší destruktivní akcí
Zrušit voucher, která vyžaduje důvod. Zrušení voucher nemaže, nastaví stavCANCELLED, čas zrušení, admin uživatele a důvod; zrušený voucher už nejde uplatnit. - Zrušení je povolené jen pro voucher bez čerpání. Částečně nebo plně čerpaný voucher se v první verzi neruší přes tuto akci.
- OWNER i SALON mají u voucherů stejná provozní práva pro editaci a zrušení. Veřejné ověření zrušeného voucheru nikdy nezobrazuje interní důvod ani metadata zrušení.
proxy.tsfiltruje požadavky na/admin/*podle platné session JWT cookie; neplatná/expirovaná cookie neprojde.- Finální autorizace probíhá server-side v admin layoutu a stránkách.
- Prisma klient používá singleton pattern pro vývoj i produkci.
- Databáze blokuje překrývající se aktivní sloty přes PostgreSQL exclusion constraint.
- Sloty s historickými rezervacemi nemažeme ani když už nejsou aktivní; pro zachování auditní stopy se místo toho archivují.
- Po každé změně Prisma schematu je potřeba spustit alespoň
npm run db:generate; při změně struktury DB inpm run db:migrate. - Technické SEO minimum je nyní pokryté přes globální metadata, per-page metadata,
robots.ts,sitemap.tsa JSON-LD. - Veřejné stránky staví metadata přes
buildPageMetadata(...)a každá musí předat vlastnípath; canonical a OpenGraph URL nesmí zůstávat na homepage pro všechny podstránky. - Smoke E2E kontroluje, že veřejné canonical/OG URL používají stejnou URL pro danou stránku a že
robots.txtsesitemap.xmlnepouští historickouhttp://ppstudio.czvariantu; zároveň vyžaduje sitemap<loc>pro indexovatelnou route/studio. Produkční canonical origin má zůstathttps://ppstudio.cz. - Veřejný layout vkládá JSON-LD
BeautySalon/WebSitepřesbuildLocalBusinessJsonLd(...), homepage vlastníWebPage, stránkaO mněpřidáváPersonpro Pavlínu Pomykalovou a detail služby přidáváServicepřesbuildServiceJsonLd(...)i samostatnýBreadcrumbListpřesbuildBreadcrumbListJsonLd(...).BeautySalonobsahuje igeosouřadnice studia aService.offersse vkládá jen při jasně číselné ceně. - JSON-LD serializer čistí
undefined,nulla prázdné hodnoty, escapuje<pro bezpečné vložení do script tagu a ponechává českou diakritiku. Délka služby v JSON-LD používá ISO 8601 helperdurationMinutesToIsoDuration(...). public/llms.txtje záměrně krátký veřejný rozcestník pro AI crawlery: uvádí kanonickou URL, hlavní veřejné landing pages a autoritativní kontakt. Nesmí odkazovat na admin routy ani tokenové self-service URL.- Web Vitals měří samostatná klientská komponenta
WebVitalsReporterv public/bookingSiteShell. PokudNEXT_PUBLIC_WEB_VITALS_ENABLEDnenítrue, komponenta se nespustí. Pokud je flag zapnutý, ale Matomo je vypnuté nebo chybí konfigurace, reporting zůstává no-op; při zapnutém Matomu odchází jen anonymní eventWeb Vitals / <metric>s ratingem a číselnou hodnotou. sitemap.tsnepoužívá jednotné „teď“ (new Date()) pro všechny URL: detail služby málastModifiedzService.updatedAt, statické stránky mají stabilní datum poslední obsahové revize.sitemap.xmlběží jako Next.js metadata route s ISR revalidací (revalidate = 86400), takže se po změnách veřejných služeb průběžně regeneruje bez ručního zásahu.- Produkční
robots.txtpouští crawl celého veřejného webu přesAllow: /; neveřejné admin a tokenové routy zůstávají blokované, aby se neindexovaly citlivé odkazy. Veřejné noindex stránky, které nemají token v path, necháváme crawl přístupné, aby si roboti mohli přečístnoindex. - Celý admin strom
/admin/*má zároveň explicitní HTML metadatarobots: noindex,nofollowvsrc/app/(admin)/admin/layout.tsx;robots.txta stránkové metadata se záměrně doplňují. - Root metadata branding (
applicationName, title template a OpenGraphsiteName) se načítá zSiteSettings.salonName;metadataBase, rootog:urla page canonical/OG URL používajísiteConfig.canonicalUrl(NEXT_PUBLIC_SITE_URLs fallbackem naNEXT_PUBLIC_APP_URL). - Veřejné SEO landing pages (
/,/sluzby,/cenik,/vouchery,/o-mne,/sluzby/[slug]) zbytečně neoznačujconnection()markerem. Request-time render má zůstat jen tam, kde opravdu záleží na aktuálním requestu nebo čerstvé slotové kapacitě, typicky u/rezervace. - Voucher PDF používá pevné kontaktní údaje z verzovaného masteru. QR verification URL se skládá z aktuálního canonical/app URL mechanismu projektu;
VOUCHER_PUBLIC_DOMAINaNEXT_PUBLIC_SITE_DOMAINslouží pouze pro kontrolu důvěryhodného hostu u requestů. - Veřejné čtení
SiteSettingsuž při renderu nezapisuje do DB; pokud singleton dočasně chybí nebo DB read selže, veřejný web a e-mailové šablony použijí bezpečné defaulty a bootstrap zápis zůstává jen v owner admin sekciNastavení. - Rezervační část má vlastní error boundary a loading fallback, takže výpadek booking vrstvy nepoškodí celý web.
- Background e-mail worker lze spustit přes
npm run email:workerjako samostatný proces; pro jednorázové dohnání fronty je k dispozicinpm run email:worker:once. - Každý background job je chráněn
processingToken; ruční okamžité doručení job nejdřív stejným způsobem atomicky claimuje. Resend REST používá pro jedenEmailLogstabilní idempotency key, zatímco obecné SMTP zůstává at-least-once (stabilníMessage-IDmůže duplicity omezit, ale nemůže je absolutně vyloučit). - Stejný
email:workerkaždých 5 minut skenuje potvrzené rezervace od aktuálního času do 26 hodin před termínem a idempotentně zapisuje jeden reminderEmailLogtypuBOOKING_REMINDER; tím dohání i pozdní potvrzení nebo výpadek původního enqueue okna. - Po přesunu termínu resetuje doménová akce
rescheduleBooking(...)oba reminder markery, takže se starý reminder neposílá pro původní termín a nový čas může znovu projít standardním enqueue flow. - Před produkční aplikací migrací je k dispozici
npm run db:check-migrations, který odhalí otevřené failed/incomplete záznamy v_prisma_migrations. - Pro systemd provoz použij
deploy/systemd/ppstudio-web.servicepro hlavní app adeploy/systemd/ppstudio-email-worker.servicepro worker. - Systemd unity i
.examplešablony povinně používají vyhrazený neprivilegovaný účet/skupinuppstudio, které připraví instalační a release skript. - Jednorázová instalace obou units je připravená v
deploy/deploy.sh. - Pro Docker Compose provoz použij
deploy/docker-compose.email-worker.yml.
- Desktop používá klasický týdenní grid se 7 dny a 30min řádky v rozsahu
06:00-20:00. - Mobil drží týdenní režim přes přehled sedmi dnů a jeden editační panel vybraného dne.
- Základní význam barev:
- zelená = běžná dostupnost
- růžová = rezervace
- písková = omezený interval, který nejde měnit přímo z planneru
- šedá = neaktivní slot
- tmavší podklad = minulý čas
- Kliknutí nebo tažení přes prázdné buňky dostupnost přidá.
- Kliknutí nebo tažení přes zelené buňky dostupnost odebere nebo zkrátí.
- Při ukládání se sousední půlhodiny automaticky sloučí do co nejmenšího počtu souvislých intervalů.
- Planner nikdy nepřepisuje rezervace ani technicky složitější sloty; pokud by změna zasáhla do chráněného úseku, vrátí srozumitelnou chybu.
- Automatický oběd trvá přesně 45 minut a počítá se v lokální zóně
Europe/Prague. Systém hledá kandidátní začátky po 15 minutách mezi11:00a13:00. - Oběd se odvozuje z publikované dostupnosti a obsazených bloků včetně úklidu; nevzniká jako samostatný slot ani rezervace v databázi. Planner jej zobrazuje jako přesný read-only blok.
- Globální přepínač je v nastavení rezervací (
SiteSettings.autoLunchEnabled). V planneru může OWNER nebo SALON pro konkrétní den přepnout režim meziAUTOaOFF; denníOFFse ukládá jako override a návrat naAUTOjej odstraní. - Pokud pro směnu nelze bezpečně umístit 45 minut, planner zobrazí varování. Veřejná rezervace ani přesun termínu nesmí spotřebovat poslední proveditelnou obědovou přestávku; stejná kontrola probíhá znovu uvnitř serializované transakce.
- Oběd může změnit polohu podle rezervací a jejich cleanupu. Ruční admin override jej může vědomě obejít a tato výjimka se audituje.
- Úplný technický popis politiky, feasibility, recommendation rankingu a authoritative ochrany je v
docs/SCHEDULE_OPTIMIZATION_MIGRATION.md.
- Salonové časy se berou jako
Europe/Prague; v UI, e-mailech a ICS má termín 09:00-10:00 zůstat 09:00-10:00 v zimě i v létě. - Při kopírování dne nebo týdne v planneru se přenáší lokální půlhodinové buňky, ne pevný počet milisekund.
- Ruční QA po změně časové logiky: vytvoř slot 09:00-10:00 v lednu a červenci, zkontroluj veřejný booking, potvrzovací e-mail i
.icspřílohu. - Ruční QA kolem DST: zkopíruj den přes poslední březnovou neděli a poslední říjnovou neděli a ověř, že zůstaly stejné lokální hodiny.