Tento dokument slouží jako detailní technická dokumentace vývoje.
Pro centralizovaný přehled hlavních route handler kontraktů používej i docs/API.md; implementace v src/app/api/**/route.ts je ale vždy finální zdroj pravdy.
- Architektonický přehled repa je v
ARCHITECTURE.md. - End-to-end veřejný booking a self-service tok je v
BOOKING_FLOW.md. - Stručný provozní deployment přehled pro Proxmox/LXC je v
DEPLOYMENT.md. - Stručný runtime přehled proměnných a prostředí je v
ENVIRONMENT.md. - Pro opakující se incidenty použij i
TROUBLESHOOTING.md. - Aktuální pravidla automatického oběda, jeho denních override, planner read modelu a authoritative booking ochrany jsou shrnutá v
SCHEDULE_OPTIMIZATION_MIGRATION.md; historické fáze v tomto dokumentu nepřepisují jeho úvodní výsledný stav.
package.jsonpoužívá SemVerMAJOR.MINOR.PATCH; aktuální release ověř vždy přímo vpackage.json.- Praktické pravidlo pro tento projekt:
PATCH: bugfix, interní refaktor bez změny kontraktu, performance tuning bez změny chování API/UI kontraktu.MINOR: nová funkce nebo rozšíření existující funkce zpětně kompatibilním způsobem.MAJOR: nekompatibilní změna (API, route contract, data contract, provozní workflow vyžadující změnu postupu).
- Průběžná práce: každá významná změna produkčního chování doplní ve stejné změně uživatelsky srozumitelný bod do
CHANGELOG.md/Unreleased; verzi vpackage.jsonneměň a historické verzované sekce neupravuj. Pravidla a výjimky stanoví ADR 0069 aAGENTS.md. - Před odevzdáním produkční změny spusť
npm run changelog:check -- --base <základní-commit-nebo-větev>. Výjimka v PR vyžaduje labelskip-changeloga řádekDůvod pro skip-changelog: <konkrétní důvod>v popisu; je určená jen pro prokazatelně nebehaviorální změny. - Příprava releasu: zvol novou SemVer verzi, přesuň obsah
Unreleasedpod novou verzovanou sekci s datem, aktualizuj případné odkazy pro porovnání verzí, vytvoř novou prázdnouUnreleaseda atomicky sjednoť verzi vpackage.json, kořenovémpackage-lock.jsona dalších skutečně používaných místech. Před commitem spusťnpm run version:check; ověřuje manifesty i všechny dokumenty označené<!-- current-version -->. Release commit obsahuje finální version bump i odpovídající release poznámky. - Pro standardní produkční rollout používej
./deploy/release.sh; skript vytváří celý release vreleases/<commit>-<čas>a atomicky přepínácurrent. Pořadí jepull -> staging npm ci -> generate -> kontrola historie -> prisma validate -> lint -> typecheck -> test:release -> build bez DB čtení aplikace -> stop webu a workeru -> ověření, že writeři neběží -> migrate deploy -> voucher:templates:bootstrap z nového release -> přepnutí current -> start webu i workeru -> health + smoke. Bootstrap je povinný a idempotentní, běží se stejným env/storage/dependencies jako nový runtime; jeho selhání je fail-closed.test:releasenespouští DB integrační scénáře nad produkčním.env; ty zůstávají povinnou CI bránou. Po migraci je rollback fail-closed: symlink se může vrátit, ale starý runtime se nad změněným schématem automaticky nespouští. - Po potvrzeném releasu helper provede retenci verzovaných adresářů: chrání
currentaprevious; další releasy ve výchozím nastavení neuchovává. Počet dalších nejnovějších release lze nastavit přes--keep-releases. Úklid nesmí běžet před health/smoke testem ani mazat jinak pojmenované provozní adresáře. - Release kontrola používá bez-DB
/api/health/livepři čekání na listener, poté readiness/api/healtha samostatnou homepage smoke URL. Při změně skriptu zachovej rozlišené provozní logy včetně HTTP statusu, aby se 500 z homepage nezaměnila za health endpoint. - Po
systemctl startnejdřív použij tichý probe webového endpointu; Next.js může několik stovek milisekund až jednotek sekund otevírat port. Teprve po otevření portu spouštěj viditelný health/smoke test. - Na
npm 11držíme vpackage.jsoniallowScriptswhitelist pro balíčky s install hooky (prisma,@prisma/engines,sharp,esbuild,unrs-resolver). Při upgradu některého z nich čekej změnu pinu a po review spusť znovunpm approve-scripts <pkg>, jinak budou releasy hlásitnpm warn allow-scripts.
VoucherTemplateje verzovaný immutable master: draft lze upravit,PUBLISHEDaINACTIVEse nikdy nepřepisují. Nový design vzniká klonem do nové verze.- Master PDF je privátní soubor v
MEDIA_STORAGE_ROOT/private/voucher-templates; databáze drží pouze relativní cestu a SHA-256. Pro first rollout spusť explicitněnpm run voucher:templates:bootstrappo migraci, nikdy při web requestu. Čistý rollout vytvoří historický neaktivníclassic-v1pro reprint/backfill starých voucherů a publikovaný strictclassic-v2pro nové vydání a tisk. Voucher.templateId,VoucherPrintBatch.templateIdaSiteSettings.voucherDefaultTemplateIdjsou referenční vazby. Default musí odkazovat na publikovanou šablonu; před deaktivací jej změň.- PRINT (216 × 105 mm), DIGITAL (vektorový crop 210 × 99 mm) i STOCK používají stejný persistentní master. Tiskový preflight strukturálně kontroluje PDF header minimálně 1.6, geometrii, OutputIntent, ICC header vhodný pro CMYK tisk (
N=4,acsp, class, color space, PCS), přesné katalogové PDF/X-4 XMP metadata (GTS_PDFXVersion=PDF/X-4bezGTS_PDFXConformance), rotaci, encryption a všechny stránky; bez externího PDF/X validátoru reportujeSTRUCTURALLY_VALIDATED, nikoli externí certifikaci.
Produkční HTML templates jsou v src/lib/email/react-email/ a sdílené komponenty v _components. npm run email:dev slouží k interaktivnímu vývoji React Email šablon, zatímco npm run email:previews vytváří regresní preview přes aplikační renderer. renderEmailTemplate(...) zůstává orchestration hranicí; React Email e-maily negeneruje ani neposílá, transport pokračuje přes současnou delivery infrastrukturu. Plain-text generování a přílohy zůstávají v orchestrace.
- Vývoj i CI standardizujeme na
Node 24 LTS. Repo drží.nvmrcs hodnotou24apackage.jsondeklarujeengines.node = ^24.0.0; před prvnímnpm installnebo po upgrade runtime si ověřnode -v. - Výchozí
npm run devpoužívá Next.js 16 dev server s Turbopackem kvůli rychlosti kompilace velké admin route. Webpack je dostupný přesnpm run dev:webpackjako stabilnější fallback při problémech s Turbopack HMR. next.config.tsnastavujeturbopack.rootna__dirname; při verzovaném release/staging buildu tak Turbopack nepovažuje sourozeneckéreleases/*/package-lock.jsonza další workspace. Při přesunu projektu neměň tuto hodnotu na pevnou absolutní cestu.- U admin formulářů nepočítej s tím, že stylování nativního
<select>/<option>bude konzistentní napříč OS. Chrome na Windows může ignorovat kontrast očekávaný z Tailwind tříd a vykreslit nečitelný dropdown; pro business-kritický výběr (např. voucher na službu) preferuj vlastní seznam/radio/button picker nad hidden inputem. - U klientských admin formulářů na Next.js 16 nepoužívej
useCallbackjen kvůli lokálním helperům typuresetForm(), pokud se nikam nepředávají jako props. Když stejný helper potřebuješ volat zuseEffecti z click handlerů, preferuj v React 19useEffectEvent; lintreact-hooks/preserve-manual-memoizationiexhaustive-depstak zůstane v souladu s React compilerem. - Pokud dev server spadne na poškozené Turbopack cache (
Failed to restore task data, chybějící.sstv.next/dev/cache/turbopack), použij:npm run dev:clean(zkontroluje historii a aktuálnost DB migrací, smaže.nexta znovu spustí dev server)npm run dev:webpack(explicitní Webpack režim; stabilnější, ale pomalejší kompilace)
- Pokud browser v dev režimu po restartu nebo invalidaci layoutu skončí na
ChunkLoadError: Failed to load chunk /_next/static/chunks/..., root shell teď provede automatický hard reload už z inlinebeforeInteractiveguardu v root layoutu, takže ochrana funguje i když spadne samotný klientský layout chunk. Guard používá krátké 15s okno se dvěma pokusy a dočasný query parametr__ppstudio_chunk_reload, který po úspěšném načtení sám odstraní; tím se nezasekne na trvalémsessionStoragelocku po prvním nepovedeném refreshi. Když ani potom načtení nepomůže, vyčisti.nextpřesnpm run dev:clean; nejde typicky o business chybu/rezervace, ale o rozjetý HMR/client cache stav. - Uploady souborů přes Server Actions respektují Next.js request body limit. Pro admin
Médiaje vnext.config.tsnastavenoexperimental.serverActions.bodySizeLimit = "10mb", protože samotný business limit obrázku je8 MBa multipart formulář přidává overhead navíc. - Kořenový
instrumentation.tsje oficiální Next.js 16 hook pro serverovou observability. Když potřebuješ víc detailů k chybám, které spadnou ještě před vlastní action logikou, přidávej je přesregister()/onRequestError, ne až do jednotlivých server actions.
resolvePragueLocalDateTime nejdříve ověřuje rozsahy data a času a následně vyžaduje přesný round-trip přes Europe/Prague. Časy v jarní DST mezeře a neplatná data se odmítají (null). Dvojznačný čas při podzimním přechodu používá dřívější výskyt.
npm run test:unitpoužívá Node test runner +tsxpreload nad quoted globemsrc/**/*.test.{ts,tsx}; quoting je záměrný, protože bez něj Bash v defaultní konfiguraci expandoval jen část stromu a coverage pak nereprezentovala celé repo. Unit soubory běží v izolovaných procesech s výchozí concurrencymin(4, dostupná paralelní jádra); pro diagnostické sériové spuštění použijTEST_CONCURRENCY=1.npm testzachovává plný lokální preflight: spustí nejdřív unit vrstvu a potom DB integrace.npm run typecheckdrží rychlou čistou TypeScript vrstvu přestsc --noEmit; je to záměrně samostatný check, aby typové regrese nebyly vidět až vnext build.npm run test:coveragepoužívác8nad tím samým runnerem a ukládá výstupy docoverage/.- U TypeScript callbacků vracejících union stavů planneru (
available/locked/booked/inactive) preferuj uflatMapexplicitní generic typuflatMap<PlannerInterval>(...). Next.js 16 build checker jinak umí vnořené větve zúžit jen podle první literal větve a shodit production build na neplatné kompatibilitěstatus. - U admin planneru rozlišuj dvě vrstvy chování: editace stále pracuje s 30min sloty/cells, ale read model pro UI posílá i
cleanupBlocks/availableBlocksv minutách, aby šlo jednu půlhodinovou buňku vykreslit po 15 minutách. availableIntervalsv planner read modelu neskládej přímo z jednotlivých slotových intervalů. Nejprve mergeujavailableBlocksv minutách a teprve potom je převáděj na celé 30min editable úseky, jinak se rozpadnou legitimně navazující sloty typu14:00–14:45+14:45–15:00.- Coverage scope je záměrně business-first:
src/features/booking/lib/**/*.tssrc/features/admin/lib/**/*.tssrc/features/admin/actions/**/*.tssrc/features/vouchers/lib/**/*.tssrc/lib/email/**/*.ts
- Report generuje formáty
text-summary,html,lcovajson-summary, takže se hodí jak pro lokální čtení, tak pro CI artefakty. - Coverage běh nezapíná
RUN_DB_INTEGRATION_TESTS=1; díky tomu měří hlavně unit/business logiku a nespadne na prostředí bez lokální databáze.npm run test:db:integrationspouští aktuálně 27 DB integračních souborů samostatně proti připravenému PostgreSQL a vždy sériově přes--test-concurrency=1; glob automaticky zahrne i další budoucí soubory.npm run test:cituto vrstvu povinně navazuje za coverage, takže ji CI nemůže omylem přeskočit. - GitHub Actions baseline je teď rozdělená do čtyř vrstev:
- hlavní
CIjako osm samostatných jobů:lint,typecheck,test,build,e2e,e2e chromium shard 2,e2e mobileae2e mobile shard 2; coverage je krok a artefakt jobutest, nikoli samostatný check Dependency Reviewpro PR dependency diffCodeQLpro statickou security analýzujavascript-typescriptSecurity Auditpro scheduled/pushnpm audit --audit-level=high
- hlavní
- Verze GitHub Actions drž vědomě blízko aktuálním major release, protože starší akce mohou na GitHub runneru skončit v compat režimu s Node deprecation warningy. Aktuální baseline v repu je
actions/checkout@v7,actions/setup-node@v7,actions/upload-artifact@v7aactions/dependency-review-action@v5. - Hlavní
CIpo doběhu ukládá artifactycoverage/aplaywright-report/; při ladění flake nebo regressí tak preferuj stažení artifactu z GitHubu před slepým lokálním rerunem. .github/dependabot.ymldrží týdenní update PR pronpmigithub-actions; pokud se změní cadence releasů nebo údržbové kapacity týmu, aktualizuj tento soubor spolu sdocs/DEPENDENCIES.md.- Pro rychlé navyšování coverage v admin vrstvě používej samostatné
*.test.tsi pro akční state moduly (src/features/admin/actions/*action-state.ts), protože i tyto server action kontrakty jsou součástí veřejného chování UI formulářů. - U server actions v
src/features/admin/actions/*.tsprioritně pokrývej validační early-return větve (invalid form payload) bez DB přístupu; je to stabilní low-flake vrstva, která rychle zavírá velké coverage mezery. - Stejný postup používej i pro
service-category-actions: validační větvecreate/updatevrací strukturovaný action state ještě před auth/DB, takže jsou vhodné pro rychlé unit testy s vysokým poměrem přínos/údržba. - Když refaktoruješ velkou klientskou komponentu typu admin planner nebo detail rezervace, vytáhni nejdřív pure helpery do samostatného souboru a přidej jim úzké Node testy. Je to bezpečnější než rovnou rozbíjet komponentu do mnoha nových child komponent bez testovatelného středu.
- Pro
booking-public/engine.tsdrž minimálně unit coverage early-fail větví bez DB závislostí (invalid startsAt,invalid phone), aby základní validační guardy nešly regresí obejít. - DB integrační testy nespouštěj proti fixním hodinám typu „za 3 dny v 09:00“, pokud běží nad sdílenou nebo již seedovanou databází. Helpery pro sloty mají nejdřív najít izolované budoucí okno bez překryvu v
AvailabilitySloti aktivníchBooking, jinak budou flaky podle aktuálních dat nebo paralelních běhů. - Stejné pravidlo platí i pro reschedule integrační testy: cílový původní i nový termín musí být před seedem ověřený proti existujícím slotům a aktivním rezervacím, jinak test může falešně skončit chybou
Nový termín koliduje s jinou aktivní rezervací.i bez regresní změny doménové logiky. - Pro ruční admin booking teď držíme dvě oddělené regresní jistoty:
booking-local-time.test.tshlídá Prague wall-clock převod abooking-manual.integration.test.tshlídá, žeslotrežim už nesmí tiše fallbacknout domanualOverride, zatímco explicitní manual režim to stále smí udělat.
- Výchozí pořadí pro nový stroj nebo čistý checkout je:
cp .env.example .env- doplnit lokální
DATABASE_URL,SHADOW_DATABASE_URL,ADMIN_SESSION_SECRET,NEXT_PUBLIC_APP_URL nvm use(nebo jiný ekvivalentní přepínač naNode 24podle.nvmrc)npm installnpm run db:generatenpm run db:migratenpm run dev
- Pro lokální vývoj preferuj
EMAIL_DELIVERY_MODE=log; reálné SMTP a veřejný Matomo tracking zapínej jen při cíleném integračním testu. - Clarity ve vývoji zapínej jen cíleně (
NEXT_PUBLIC_CLARITY_ENABLED=true+NEXT_PUBLIC_CLARITY_PROJECT_ID), default má zůstat vypnutý. - Google Ads tag ve vývoji zapínej jen cíleně (
NEXT_PUBLIC_GOOGLE_ADS_ENABLED=true+NEXT_PUBLIC_GOOGLE_ADS_ID=AW-...), default má zůstat vypnutý. - Meta Pixel ve vývoji zapínej jen cíleně (
NEXT_PUBLIC_META_PIXEL_ENABLED=true+NEXT_PUBLIC_META_PIXEL_ID), default má zůstat vypnutý. - Recovery admin přístupu prováděj pouze offline příkazem
npm run admin:recover-owner -- --email owner@example.com --name 'Jméno' --confirm < heslo.txt; webový bootstrap login neexistuje. - README na GitHubu má fungovat jako rozcestník i rychlý onboarding. Když měníš setup, deploy nebo monitoring workflow, promítni změnu do
README.mda udržuj v něm krokový postup, ne jen seznam odkazů.
Předtištěné voucherové série používají modely VoucherPrintBatch, VoucherStockItem a VoucherStockAuditLog. VoucherStockItem.code je rezervovaný globálně proti Voucher.code; alokace kódu i čísla série probíhá uvnitř serializované PostgreSQL transakce s transaction advisory lockem. Běžné vystavení voucheru proto nesmí generovat kód mimo transakci, která následně zapisuje Voucher.
Tisková série vzniká atomicky v routě /admin/vouchery/predtistene, její PDF je deterministický výstup existujících kusů a každý kus má jednu stránku se stejným masterem, kódem a QR. Batch PDF lze stáhnout opakovaně pouze ve stavu PENDING_PRINT; po přechodu do RECEIVED nebo CLOSED route download odmítne a další fyzický tisk vyžaduje novou sérii. Stavový workflow batch je PENDING_PRINT → RECEIVED → CLOSED, workflow kusu PENDING_PRINT → AVAILABLE → ACTIVATED|VOID; příjem, aktivace, VOID a uzavření série jsou auditované a aktivace je idempotentní podle skladového kusu/kódu. SALON nemůže sérii založit, stáhnout ani uzavřít, ale může přijmout aktivaci a označit dostupný kus jako VOID.
Při změně schématu dodrž standardní Prisma workflow: před migrací npx prisma migrate status, potom npx prisma migrate dev --name <název>, a po migraci npx prisma validate, npx prisma generate a npx prisma migrate status. DB scénář Voucher Stock se spouští izolovaně přes RUN_DB_INTEGRATION_TESTS=1 a je určen pro DEV databázi.
- Při změně mobilního admin UI drž minimální výšku hlavních dotykových ovladačů alespoň
2.75rema nenechávej dlouhou řadu filtrů zalomit se do nečitelných řádků: na telefonu může být vodorovně posuvná, na širším breakpointu se vrací běžné zalomení. U týdenního planneru musí fixed sheet i publish lišta počítat senv(safe-area-inset-bottom). src/appobsahuje pouze routy, layouty a route handlers.src/componentsdrží čistě sdílené stavební prvky.src/featuresseskupuje konkrétní produktové oblasti:homepublicbookingadminvouchers
src/libobsahuje infrastructure kód bez prezentační logiky.src/lib/mediadrží infrastrukturní vrstvu pro lokální ukládání a čtení médií.src/configdrží metadata, navigaci a validované prostředí.src/contentdrží editovatelná data veřejného webu odděleně od layoutu a route souborů.- Fallback texty ve
src/content/public-site.tsmusí působit jako finální veřejný obsah (bez interních výrazů typu placeholder/TODO), aby i při výpadku DB copy zůstal web důvěryhodný a produkčně použitelný. - U větších admin modulů nedrž zároveň route orchestration, server fetch logiku, pure business helpery a JSX v jednom souboru. Aktuální baseline je:
src/features/admin/lib/admin-route-factories.tsxjen pro routing a guardysrc/features/admin/lib/admin-settings-page-data.tspro server-only read model nastavenísrc/features/admin/components/*-helpers.tspro čistou synchronní logiku testovatelnou bez React renderusrc/features/admin/components/*.tsxpro samotnou kompozici UI
(public)pro prezentační web.(booking)pro rezervace bez míchání admin logiky.(admin)pro backoffice.- Booking-specifické globální selektory (např. landscape tweak pro booking header/sticky CTA) drž v
src/app/(booking)route-level CSS importovaném v booking layoutu, ne v rootsrc/app/globals.css, aby se tyto styly nenačítaly na homepage. - Další vnitřní route group
(protected)uvnitř adminu chrání sekce vyžadující session. - Veřejné booking flow používá server-loaded page + klientský wizard + server action pro finální zápis.
- U všech veřejných i admin formulářů nad Next.js Server Actions počítej s deploy skew: aplikace čte
deploymentIdzNEXT_DEPLOYMENT_IDnebo fallbackuDEPLOYMENT_VERSION/GIT_HASH, ale plná ochrana funguje jen když všechny produkční instance stejného buildu sdílí i stejnýNEXT_SERVER_ACTIONS_ENCRYPTION_KEY. deploy/release.shzapisuje release identifikátory nejen do build env, ale i do.release-envpro systemd runtime. Pokud měníš release flow nebo service unitu, zachovej, ženext build,next startainstrumentation.tsvidí stejný deployment identifikátor; jinak seppstudio.next.registerappstudio.next.request-errorrozjedou s realitou.instrumentation.ts:onRequestErroruž proFailed to find Server Actionloguje sanitizované request headers (x-deployment-id, proxy/IP metadata, user-agent), route context (routeType: action), fingerprintNEXT_SERVER_ACTIONS_ENCRYPTION_KEYa bezpečné shrnutínext-actionheaderu. Při dalších úpravách zachovej zásadu, že do těchto logů nesmí spadnout raw booking tokeny, celé query stringy ani plné interní action ID.- Ve veřejném booking flow platí focus pravidlo: klik na den v kalendáři má převést fokus na sekci
Dostupné časy, zatímco klik na konkrétní čas má převést fokus na první input kontaktního kroku. Při dalších UX úpravách tenhle sled zachovej, aby zůstal konzistentní pro klávesnici i mobilní scroll. - Dny v booking kalendáři bez jediného reálně volného času (
all slots disabled) mají zůstat neinteraktivní (disabled) aaria-labeldnů má používat lidský formát přesformatDateKeyLabel(...), ne surovéYYYY-MM-DD. - Error a empty stavy veřejného booking flow musí být akční: u chybějících služeb/termínů nebo konfliktů nabídni konkrétní další krok (
/kontakt,/ceniknebo návrat do příslušného kroku) a drž klidný prémiový tón PP Studia ve Zlíně. - U
useActionStateflow na veřejných tokenových stránkách nerevaliduj právě otevřenou route jen kvůli cache invalidaci. V Next.js 16 to po server action může vyvolat route refresh a resetovat lokální success/error stav klientské komponenty dřív, než se vykreslí. - U veřejných analytics trackerů, které kombinují inline
next/scriptbootstrap a navazující App RouteruseEffect, drž sdílený runtime marker o tom, že už byla bezpečná route zapsaná do fronty. Bez něj jsou tokenové self-service routy náchylné na race mezi hydratací, Suspense a E2E spy vrstvou. - U admin
useActionStateformulářů, které mění booking stav, nenechávej response čekat na externí síťové side-effecty (např. Pushover API). Notifikace posílej non-blocking (void ...catch(...)), aby se UI nezaseklo napendingpři pomalé třetí straně. - U
useOptimistic(React 19 / Next.js 16) dispatchuj optimistic update pouze uvnitřstartTransition(...)nebo action contextu. Přímé volání mimo transition v event helperu vede k runtime warninguAn optimistic state update occurred outside a transition or action. - Veřejné booking routy nově obsahují i bezpečný provozní action flow
/rezervace/akce/[intent]/[token], který renderuje serverovou validaci tokenu a klientský potvrzovací panel nad server action submittem. - Veřejné API nyní obsahuje i route handler
/api/calendar/owner.ics, který vrací chráněný.icsfeed pro Apple Calendar subscription; endpoint je veřejný jen přes tajný token v URL a nepoužívá session auth. - Klasické admin POST route handlery (
/api/auth/login,/api/auth/logout, owner resend invite, admin voucher lookup POST) nespoléhej jen naSameSite=Lax; drž explicitníOrigin/trusted host kontrolu přesisSameOriginAdminRequest(...), aby cross-origin form submit skončil dřív než auth nebo mutace. - Veřejný
GET /api/health/liveje bez-DB liveness probe. VeřejnýGET /api/healthje readiness probe s jedinýmSELECT 1a minimálním JSON kontraktem; nesmí číst frontu ani vracet worker, incident nebo release metadata. - Detailní
GET /api/health/diagnosticszachovává provozní snapshot fronty, workeru, incidentů a releasu, ale musí zůstat dostupný pouze owner session. Selhání detailního Prisma read modelu vrací ownerovi200/warningsEMAIL_HEALTH_UNAVAILABLEa detail chyby se loguje pouze serverově. - DB failure alert z health endpointu je best-effort non-blocking dispatch s vlastním desetiminutovým in-memory cooldownem. Nečekej na Pushover v requestu; cooldown je lokální pro jeden runtime proces, proto v multi-instance provozu není náhradou centrálního alertingu.
/rezervacepoužíváconnection()a renderuje se request-time, aby ručně publikované sloty nebyly zafixované do build outputu.- Veřejné SEO landing pages (
/,/sluzby,/cenik,/vouchery,/o-mne, detail/sluzby/[slug]) naopak nenuť do request-time režimu jen kvůli read modelům nebo JSON-LD. Next.js 16 výslovně doporučuje nedávatawait connection()přímo do page komponenty, pokud tím jen zbytečně bráníš statickému shellu a metadata nepotřebují runtime request kontext. - Klientské UX booking flow je soustředěné v
src/features/booking/components/booking-flow.tsx, ale rychlé decision bloky jsou rozsekané do menších komponent:CategorySelectpro první rozhodnutí nad kategoriemiSuggestedSlotspro nejbližší jedním klikem rezervovatelné časyStickyCTApro mobilní pokračování / submit bez ztráty kontextuBookingConfirmationPanelpro post-submit stav se status blokem, dominantním termínem, CTA a kontaktem
- Veřejný katalog slotů v
src/features/booking/lib/booking-public/catalog.tsnově používá helperbooking-slot-availability.ts, který:- slučuje navazující kompatibilní publikované sloty do jednoho delšího veřejného okna,
- zachovává mapu původních segmentů pro správné
slotIdpři submitu, - počítá
bookedIntervalspodle skutečných aktivních rezervací překrývajících daný čas, ne jen podle relaceBooking.slotId.
- Tokenová správa rezervace (
/rezervace/sprava/[token]) volá katalog s vynecháním právě spravované rezervace. Díky tomu může nabídnout i dřívější start v navazujícím volném bloku před původním termínem; serverová validace ale dál kontroluje celý coverage řetězec a všechny ostatní aktivní bookingy. - Admin detail rezervace používá pro reschedule sloty stejný princip (
excludeBookingIdvgetPublicBookingCatalog). Bez toho by vlastní aktivní booking blokoval původní slot a UI by nenabídlo posun o 30 minut dřív přes volné okno před začátkem. - Admin detail rezervace nyní podporuje i přímou výměnu služby na existujícím bookingu. Tato mutace musí zůstat serverově omezená na
PENDINGaCONFIRMED, přepočítatserviceDurationMinutes,cleanupMinutes,cleanupBlockMinutes,scheduledEndsAt,blockedUntila zapsat audit doBookingStatusHistory. - Změna služby nesmí tiše obejít doménová omezení. Při další úpravě zachovej validaci proti stávajícímu času: nová služba se musí vejít do coverage aktuálního slotu nebo interního override, nesmí porušit
allowedServicesa nesmí projít přes navázaný službový voucher pro jinou službu. - Horní hlavička detailu rezervace drž jako kompaktní dvouřádkový toolbar: nahoře návrat + stav/kanál vlevo a rychlé akce vpravo, dole jméno klientky a sekundární řádek
služba · délka · termín; termín nemá být vykreslený v samostatném velkém boxu. - Playwright booking fixture v
tests/e2e/helpers/fixtures.tsseeduje termíny dynamicky podle aktuální booking policy (bookingMinAdvanceHours,bookingMaxAdvanceDays,bookingCancellationHours), aby testované self-service scenáře vždy běžely uvnitř skutečného online okna pro rezervaci/storno/přesun. - Protože E2E suite běží napříč více specy se 2 workery, fixture sloty se nesmí spoléhat na jeden sdílený čas.
createCatalogFixture()vytváří trojici availability slotů v transakci, start rozhazuje hashovaně podlerunIda přiAvailabilitySlot_active_time_window_exclzkusí další kandidát. - Test runtime (
NODE_ENV=test) záměrně vypíná Prisma client stdout/stderr query error logy vsrc/lib/prisma.ts; zachycené a retrynuté seed konflikty by jinak šuměly ve výstupu E2E/DB testů, i když samotný test projde. - U E2E výběru slotů nepočítej s unikátním textovým labelem času; při stejných časech v různých blocích je selector podle textu (
.first()) flaky. V testech ověřuj vybraný hiddenslotIdproti očekávané fixture hodnotě. - U booking management E2E helperů nepočítej s tím, že fixture předem uhádne přesný label nebo dokonce konkrétní nabízený slot. Sdílená test DB může do katalogu propsat jinou sadu volných časů; scénáře proto stav nad UI potvrzují přes hidden
slotId/newStartAtvybraného skutečně dostupného tlačítka, ne přes pevný accessible name. - E2E fixture slot labely musí formátovat datum z konkrétního začátku daného slotu, ne ze začátku celé testovací dvojice. Při časech kolem půlnoci by jinak success slot hledal předchozí den a Playwright by mohl kliknout na stale slot ze staršího běhu.
- Při vytvoření rezervace nebo přesunu nad coverage řetězcem už nestačí rozřezat jen single-slot případ. Když booking začíná uprostřed prvního segmentu nebo končí uprostřed posledního segmentu, engine musí rozdělit i tyto krajní coverage sloty, jinak admin planner falešně vykreslí volný okraj jako
lockedremainder. - Pro dočištění starších dat je v
scripts/repair-legacy-chained-slots.mjszáměrně konzervativní repair flow:- opravuje jen plain published anchor sloty s jedinou navázanou rezervací,
- umí vytvořit jen bezpečný
beforea/neboafterfragment bez zásahu do booking FK, - případy s více bookingy na jednom slotu nechává jako
skipped, protože tam už nejde bez doménového rozhodnutí garantovat bezpečný automatický split.
- Pro editovatelnost plain published slotu v planneru neber
CANCELLEDbooking jako blokující. Pokud na slotu nezůstává aktivní nebo dokončená návštěva ani jiné omezení, planner ho má ukázat jako běžnou dostupnost. Write model ale nesmí historický slot smazat natvrdo; při publish mutaci ho archivuje a případný nový aktivní interval založí zvlášť. - Při slučování sousedních published slotů po storno/reschedule nejdřív vyřaď slučovaný sousední slot z active DB constraintu (
AvailabilitySlot.status = ARCHIVED) a až potom rozšiřuj anchor interval. Opačné pořadí vyvolá PostgreSQLAvailabilitySlot_active_time_window_excl, protože se dvě aktivní okna na okamžik překryjí uvnitř jedné transakce. - Stav rezervace
COMPLETEDje provozní uzávěrka po proběhlé návštěvě, ne nástroj pro předběžné odbavení. Admin akceHotovosmí projít až poscheduledEndsAt, protože aktivní blokace kapacity a dashboardové volné úseky počítají jenPENDINGaCONFIRMED. - Dashboardová timeline dne načítá pro zobrazení i
COMPLETED, aby hotové návštěvy zůstaly v dnešním plánu viditelné. Výpočet volných oken ale ořezává začátek na aktuální čas, takže minulá dostupnost se nikdy nepropíše jako akční volný termín. - Alerty v admin dashboardu (
Vyžaduje pozornost) mají zůstat jen pro akční provozní problémy (pending bookings, failed e-maily, overdue rezervace). Samotná nulová dostupnost dnes/zítra se nesmí eskalovat jako warning/problem alert. - Empty state v
Nejbližší volné termínydrž neutrální tón (Momentálně nejsou publikované žádné nadcházející volné termíny.). Pokud existují budoucíDRAFTsloty, ukaž i počet návrhů čekajících na publikování s přímým odkazem do plánovače dostupnosti. - Admin planner
Volné termínynačítá pro zobrazeníPENDING,CONFIRMEDiCOMPLETED; dokončené rezervace jsou vizuálně tlumené, ale stále vysvětlují, proč historický úsek není běžná editovatelná dostupnost. - Drag editace v planner gridu musí zůstat funkční i pro mobilní pointery
touch/pen; nespoléhej jen naMouseEvent.buttons === 1a u buněk držtouch-action: none, aby tažení nepřebíjel nativní scroll gesture. - UX polish týdního planneru drž výslovně jako
density pass, ne jako novou IA: hlavní priorita je plocha pro grid, datum týdne jen jednou v toolbaru, pravý panel ve 3 kartách a legenda jako malý sekundární prvek až u detailu výběru. - Pro další úpravy planneru preferuj jemné změny kontrastu a rytmu mřížky před přidáváním dalšího chrome: výraznější celé hodiny, subtilnější půlhodiny, jasnější selected day/block a minimum textu přímo uvnitř gridu.
- FullCalendar planner ukládá změnu dostupnosti průběžně přes
applyPlannerSelectionAction(); při chybě nabídne opakování nebo obnovení uloženého stavu. - Planner mutace v
src/features/admin/lib/admin-slots/mutations.tsběží serializable a musí počítat s retry na PrismaP2034/TransactionWriteConflict; paralelní CI cleanup nebo jiný zápis doAvailabilitySlottabulky nesmí shodit změnu dostupnosti na první konflikt. - Admin detail klientky je provozní CRM pohled nad
getAdminClientDetailData(...)plus samostatné server actions proClient.internalNotea kontaktní údaje. Při úpravách neměň route kontrakt/admin/klienti/[clientId]//admin/provoz/klienti/[clientId]a nepřidávej nová pole do Prisma schematu kvůli preferencím. Úprava kontaktu musí validovat e-mail/telefon, hlídat unikátníClient.emaila propsat e-mail/telefon jen do aktivních rezervací (PENDING/CONFIRMED), protože booking e-maily dál čtouBooking.clientEmailSnapshot; proběhlé rezervace (COMPLETED/CANCELLED/NO_SHOW) zůstávají auditně beze změny. Každý propis kontaktu do aktivní rezervace zapisuj doBookingStatusHistory(reason: Kontakt klientky upraven) včetně metadat původního/nového e-mailu a telefonu.Poslední návštěvav detailu znamená poslední minulou rezervaci ve stavuCOMPLETED, neClient.lastBookedAt, protoželastBookedAtse aktualizuje už při vytvoření rezervace.CRM souhrnpočítej přessrc/features/clients/lib/client-crm-summary.ts; pro platby nepíš novou aritmetiku mimo tento helper a sdílenýgetBookingPaymentSummary(...).Uhrazenoje součet skutečných plateb a voucherových čerpání, aleNeuhrazenosmí zahrnout jenCOMPLETEDrezervace nebo aktivníPENDING/CONFIRMEDrezervace se začátkem v minulosti, aby budoucí termíny nevypadaly jako dluh. UI drž kompaktní jako provozní workspace: žádné hero měřítko, tmavé card UI, jemné bordery, malé uppercase labely a krátké řádky historie bez zbytečných placeholderů poznámek. - Stejnou definici drž i seznam klientek na
/admin/klientia/admin/provoz/klienti: sloupec i řazeníPoslední návštěvase musí opírat o poslední minulou rezervaci ve stavuCOMPLETED, ne o profilovéClient.lastBookedAt. - Totéž platí pro starší read model
getClientsData(...)vsrc/features/admin/lib/admin-data.ts, pokud se ještě někde renderuje fallback / legacy sekce klientek přesAdminSectionPage. - Root
/admin/email-logyje kompatibilní redirect na/admin/logy?view=emails; e-mailový seznam patří do společného read-modelugetAdminLogsData(...). Route layout vsrc/app/(admin)/admin/email-logy/layout.tsxzůstává kvůli detailu[emailLogId]; do jeho potomka nepřidávej dalšíAdminShell, protože Next.js rodičovské layouty vnořuje a výsledkem by bylo dvojité levé menu. - Obecný fallback
AdminSectionPageuž v routing vrstvě neexistuje. Pokud přidáváš novou admin sekci, přidej jí explicitní obsluhu docreateAdminSectionRoute(...)nebo vlastní statický route soubor; nenechávej „dočasný“ generic renderer jako skrytý runtime fallback. - Intro copy v adminu drž provozně a konzistentně: krátký
eyebrowjako kontext sekce, jednoduchýtitlejako pracovní název a jednovětýdescriptionbez marketingového tónu. - Nedrž mrtvé read-modely ani komponenty jen kvůli historii rout. E-maily, Pozornost, Události a Systém musí zůstat ve společném feedu s jednotnými filtry a stránkováním.
- Historie návštěv v detailu klientky rozlišuje původ poznámek u rezervace:
KlientkaproBooking.clientNoteaInterněproBooking.internalNote. Pokud existují obě, zobraz obě; nevracej se k prioritěinternalNote ?? clientNote, protože ta ztrácí provozní kontext. - Seznam klientek v adminu je click-to-open: na desktopu otevírá detail celá tabulková řádka, na mobilu celá karta. Tlačítko/štítek
Detailzůstává jen jako vizuální affordance stejné navigace. - Primární CTA
Vytvořit rezervaciv detailu klientky musí proOWNERiSALONvést do existujícího ručního booking workspace/admin/.../rezervace?create=1&clientId=.... Prefill klientky je jen UX zkratka: booking action i nadále musí projít stejnou server-side validací dostupnosti, překryvů, služeb, notifikací a audit trailu. - Stejný princip deep-link prefill teď používá i dashboard a planner:
/admin/.../rezervace?create=1&date=YYYY-MM-DD&time=HH:MMotevře ruční booking drawer s předvyplněným ručním termínem, ale bez obcházení backend validace. Pokud měníš tento tok, drž query kontraktcreate/clientId/date/timekompatibilní napříč dashboardem, plannerem a klientským detailem. - Dashboard
Dnešní plána kartaDalší rezervacezáměrně obsahují přímé provozní akceVolat,E-mailaNová rezervace. Cíl je méně klikání při obsluze nejbližších klientek; při dalších úpravách nepřeváděj tyto akce zpět jen do detailu rezervace. - Sekce
Nejbližší volné termínyaDayInspectorv planneru jsou nově akční, ne jen orientační. Volné okno nebo vybraný blok musí umět otevřít stejný ruční booking drawer s předvyplněným termínem, aby kosmetička nemusela čas přepisovat mezi sekcemi. - Stejný helper řeší i backend validaci souvislého pokrytí intervalu při
createBookingWithEngine(...)arescheduleBooking(...); když upravuješ pravidla slotů, drž veřejný katalog a backend coverage logiku v sync. - Coverage validace musí umět spadnout z preferovaného
slotIdna segment, který skutečně obsahuje nový začátek. Tohle je důležité pro posun rezervace na dřívější start přes volný blok před původním termínem. - Stabilizační refaktor z
2026-04-24rozděluje dříve monolitické booking/admin soubory do menších interních modulů při zachování stávajících entrypointů:src/features/booking/lib/booking-public.tsje façade nadbooking-public/shared.ts,catalog.ts,engine.ts,notifications.tssrc/features/booking/components/booking-flow.tsxdrží jen stav a orchestraci kroků; jednotlivé UI bloky jsou vsrc/features/booking/components/booking-flow/*src/features/admin/lib/admin-slots.tsje façade nadadmin-slots/time.ts,helpers.ts,queries.ts,mutations.ts,types.ts
- Při dalším refaktoru těchto oblastí drž kompatibilní veřejné exporty v původních entrypointech, aby se nerozbily existující importy v actions, routech a admin UI.
src/app/robots.tsasrc/app/sitemap.tspoužívají metadata route API v App Routeru.robots.txtisitemap.xmlmusí skládatHost,Sitemapa<loc>URL přes kanonický SEO originsiteConfig.canonicalUrl(NEXT_PUBLIC_SITE_URLs fallbackem naNEXT_PUBLIC_APP_URL), aby byly technické SEO routy konzistentní s JSON-LD a page metadata.src/app/sitemap.tsje explicitně ISR metadata route (export const revalidate = 86400), takže sesitemap.xmlgeneruje dynamicky a obnovuje nejvýše jednou denně bez plného redeploye.- U Next.js 16 route segment config exportů (
revalidate,dynamic,fetchCache, atd.) používej v route souboru přímo literál nebo jinou staticky analyzovatelnou hodnotu; výraz typu60 * 60 * 24může přinext buildskončit chybouInvalid segment configuration export detected. src/app/sitemap.tsmusí nastavovat realistickélastModifiedhodnoty: detail služeb zService.updatedAt, statické stránky ze stabilního data poslední obsahové revize (ne plošněnew Date()pro všechny URL).- Veřejné stránky používají
buildPageMetadata(...)a vždy předávají vlastnípath; helper z něj skládá canonical a OpenGraph URL zesiteConfig.canonicalUrl. Nevkládej globální canonical do root layoutu, protože by ho zdědily podstránky. - Playwright smoke test pro veřejné stránky ověřuje, že canonical a
og:urlmíří na stejnou stránkovou URL. Discovery test zároveň hlídáHost/Sitemapvrobots.txt, sitemap<loc>na stejném originu včetně indexovatelné route/studioa absenci historickéhttp://ppstudio.czURL. src/features/public/lib/public-services.tsnesmí předpokládat, že Prisma relation include vždy vrátí neprázdnou kategorii ve stejném okamžiku jako službu; při souběžném testovacím cleanupu může relation fetch krátce vrátitnull, takže mapování veřejných služeb musí mít fallback kategorie a nesmí spadnout na.name.- Stránka
/o-mneje vsrc/features/public/components/about-page.tsxzáměrně vedená jako klidná premium landing page, ale density pass z2026-05-03drží kompaktnější rytmus: menší sectionpy, střídmější card padding, těsnější návaznost blokůMůj příběhaMůj přístup, kompaktnější FOR LIFE & MADAGA pills a nižší certifikační gallery. Při dalších úpravách zachovej obsahovou skladbu a CTA routy zaboutContent, nevracej stránku do roztahaného hero/card rytmu. - Strukturovaná data jsou v
src/features/public/components/seo-json-ld.tsx: veřejný layout vkládáBeautySalon/WebSite, homepage vkládá vlastníWebPage, stránkaO mněpřidáváPersonpro Pavlínu Pomykalovou a detail služby vkládá samostatněServiceaBreadcrumbList.BreadcrumbListskládej přesbuildBreadcrumbListJsonLd(...), aby se absolutní URL tvořily jednotně z kanonického SEO originu (siteConfig.canonicalUrl;NEXT_PUBLIC_SITE_URLs fallbackem naNEXT_PUBLIC_APP_URL) bez zdvojených lomítek. JSON-LD se serializuje přesJSON.stringify(...).replace(/</g, "\\u003c"); adresa salonu vBeautySalonaareaServedu služby se berou zgetPublicSalonProfile()/SiteSettings, aby držely stejnou hodnotu jako veřejný kontakt.BeautySalonnavíc obsahujegeos přesnými souřadnicemi studia.Service.offersse generuje jen pro jasně číselnou cenu služby. public/llms.txtje veřejný strojově čitelný rozcestník pro LLM crawlery. Drž ho stručný, faktický a stabilní: hlavní landing pages, kanonický web, kontaktní fakta a výslovné upozornění, že admin/tokenové URL nejsou určeny k citaci ani navigaci.src/config/site.tsdrží globální fallback SEO popis a kontakty pro metadata a technické routy. Udržuj ho věcný k PP Studiu ve Zlíně a nepoužívej placeholder kontakty; produkční fallback jeinfo@ppstudio.cz,+420 732 856 036aSadová 2, 760 01 Zlín.src/app/robots.tsje v produkčním nastavení otevřený pro celý veřejný web (Allow: /); blokované mají zůstat jen neveřejné admin a tokenové self-service cesty. Noindex veřejné stránky bez tokenu v path neblokuj vrobots.txt, aby si robot mohl přečíst jejich metadata.- Veřejné JSON-LD upravuj přes
src/features/public/components/seo-json-ld.tsx; data ber z public read modelů agetPublicSalonProfile(), ne z duplicitních hardcoded kontaktů. Testuj serializer i helpery vseo-json-ld.test.ts, zejména CZK cenu, ISO duration, BreadcrumbList URL a absenci prázdných hodnot. - Web Vitals reporting drž izolovaný v
WebVitalsReporter; neposílej URL, klientská data ani booking tokeny. Pokud bude potřeba jiný backend než Matomo, nejdřív přidej ADR a bezpečnostní návrh endpointu. - Admin routy pod
src/app/(admin)/admindědí explicitnímetadata.robotsnoindex/nofollow z admin layoutu. Nové admin obrazovky drž pod tímto stromem; pokud vznikne neveřejná route mimo něj, musí dostat vlastní noindex metadata. - Matomo client tracking je v
src/features/analytics/*a inicializuje se přessrc/components/layout/site-shell.tsx. Při lokálním vývoji nastavNEXT_PUBLIC_MATOMO_ENABLED=true,NEXT_PUBLIC_MATOMO_URLaNEXT_PUBLIC_MATOMO_SITE_ID; bez kompletní konfigurace je helper bezpečný no-op. Web Vitals mají navíc samostatný flagNEXT_PUBLIC_WEB_VITALS_ENABLED(defaulttrue), takže je lze vypnout bez vypnutí celého Matomo trackingu. - Clarity client tracking je v
src/features/analytics/clarity*.ts(x)a inicializuje se také přesSiteShell(next/script,lazyOnload); bez kompletní konfigurace (NEXT_PUBLIC_CLARITY_ENABLED,NEXT_PUBLIC_CLARITY_PROJECT_ID) je helper bezpečný no-op. - Google Ads client tracking je v
src/features/analytics/google-ads*.ts(x)a inicializuje se také přesSiteShell(next/script,afterInteractive); bez kompletní konfigurace (NEXT_PUBLIC_GOOGLE_ADS_ENABLED,NEXT_PUBLIC_GOOGLE_ADS_ID) je helper bezpečný no-op. - Meta Pixel client tracking je v
src/features/analytics/meta-pixel*.ts(x)a inicializuje se také přesSiteShell(next/script,afterInteractive); bez kompletní konfigurace (NEXT_PUBLIC_META_PIXEL_ENABLED,NEXT_PUBLIC_META_PIXEL_ID) je helper bezpečný no-op. - Meta Pixel helper v
src/features/analytics/meta-pixel.tsje zároveň jediné místo profbqeventy a sanitizaci payloadu; nové eventy nepřidávej jako ad-hocwindow.fbq(...)v komponentách. SiteShellpřed renderemMatomoTrackerčte přítomnost admin session cookieppstudio-admin-session; pokud je cookie přítomná,MatomoTrackerse renderuje sdisableda nenačtematomo.jsani init script. Cíl je vyloučit vlastní návštěvy administrace i na veřejných routách bez DB dotazu.- Stejný
disabledguard vSiteShellpoužívá iClarityTracker, takže přihlášený admin není měřený ani na veřejných stránkách. - Stejný
disabledguard vSiteShellpoužívá iGoogleAdsTracker, takže přihlášený admin není měřený ani na veřejných stránkách. - Stejný
disabledguard vSiteShellpoužívá iMetaPixelTracker, takže přihlášený admin není měřený ani na veřejných stránkách. GoogleAdsTrackerv App Routeru neposílá jen úvodní inline snippet; po klientské navigaci opakujegtag('config', tagId, { page_path, page_title }), aby se pageview neztratily při přechodech bez full reloadu.page_pathse skládá přes stejnou sanitizaci jako Matomo, takže nepropustí tokenové self-service URL ani citlivé query parametry.MatomoTrackerbootstrapuje_paqpřes inlinenext/scriptna strategiiafterInteractive, aby se bezpečnýsetCustomUrlspolehlivě propsal i v pomalejších CI/prohlížečových bězích ještě před prvním self-service klikem. Externímatomo.jsse načítá stejnou strategií. Inline skript pageview neposílá; první načtení i App Router navigace mají jediného odesílatele v klientském layout efektu.- Pageview tracking v App Routeru poslouchá
usePathname()auseSearchParams(). Layout efekt posílá první i další povolené pageview se sanitizovanou URL, bezprostředně před každýmtrackPageViewji znovu zapíše do Matomo fronty a deduplikuje podle poslední skutečně odeslané cesty; inline bootstrap pouze nastaví tracker a bezpečnou URL. - Tokenové self-service booking route (
/rezervace/sprava/*,/rezervace/storno/*,/rezervace/akce/*) neposílají pageview s tokenem.MatomoTrackerna nich může pouze inicializovat_paq, aby šly z klientských handlerů poslat bezpečné neosobní eventy bez raw URL. - Funnel a CTA eventy se smí volat jen v client handlerech nebo efektech po úspěšné akci; neposílej jména, e-maily, telefony, poznámky, tokeny ani raw URL s citlivými parametry.
- Meta Pixel standardní eventy drž anglicky podle konvence platformy (
PageView,ViewContent,InitiateCheckout,Schedule); custom Meta eventy používej stabilně v PascalCase (BookingServiceSelected,BookingDateSelected,BookingTimeSelected,BookingContactStarted). - Meta Pixel v aktuální verzi měří tyto neosobní kroky:
- detail služby:
ViewContent - výběr termínu nebo přechod do kontaktního kroku booking flow:
InitiateCheckout - výběr služby:
BookingServiceSelected - výběr dne:
BookingDateSelected - výběr času:
BookingTimeSelected - první interakce s kontaktním krokem:
BookingContactStarted - úspěšně vytvořená rezervace:
Schedule(bezvalueacurrency, pokud není k dispozici jednoznačná finální cena)
- detail služby:
- Matomo eventy pojmenovávej primárně česky (
categoryiaction) a drž je stabilní v čase; anglické názvy používej jen tam, kde jde o standardní technický termín (Web Vitals,CLS,LCP). URezervace / Kontakt pole chybanesenamejen název pole a bezpečný důvod (povinné,příliš krátké,neplatný formát), nikdy hodnotu ani text chyby. - V kontaktním kroku rezervace (
BookingContactStep) se sledují jen neosobní interakce s poli (fokus,začátek vyplnění,chyba) a název pole (fullName/email/phone); nikdy neposílej obsah zadaných hodnot. - Server-side dashboard reporting je v
src/lib/analytics/matomo.tsa je záměrně oddělený od klientského helperu. Používá pouzeMATOMO_*env bezNEXT_PUBLIC_,import "server-only", Matomo metodyVisitsSummary.get,Events.getAction,Referrers.getReferrerTypeaReferrers.getCampaigns, a každý request cachuje přesfetch(url, { next: { revalidate: 300 } }). getDashboardAnalytics()skládá první funnel krokviewedz Matomo pageview reportuActions.getPageUrlspro/rezervacea/rezervace?..., zatímco krokyservice,term,contact,submitted,createdmapuje z event labelsRezervace / Služba vybrána,Rezervace / Čas vybrán,Rezervace / Kontakt zahájen,Rezervace / Odeslána rezervaceaRezervace / Vytvořena; protožeEvents.getActionmůže vracet jen action label, agregace akceptuje i zkrácené labely bez prefixuRezervace / .... Hlavní KPIconversionsje stejné číslo jakofunnel.created, aby se metrika rezervací nerozcházela s funnel krokem.getDashboardAnalytics()vrací icontactStepQualitynad eventyRezervace / Kontakt zahájen,Kontakt pole fokus,Kontakt pole vyplnění začátek,Kontakt pole chyba; API počítá počty i poměryfocusRate,inputRate,errorRatevůčiKontakt zahájen.- Ve veřejném booking flow drž dva typy Matomo měření odděleně: pageview
/rezervacejako vstup do funnelu a potom hlavní funnel eventy (Služba vybrána,Čas vybrán,Kontakt zahájen,Odeslána rezervace,Vytvořena) plus diagnostické/provozní mikro-eventy (Datum vybráno,Formulář chyba,Termín konflikt při odeslání,Bez služeb,Bez termínů,Kontakt pole fokus,Kontakt pole vyplnění začátek,Kontakt pole chyba). Do eventnameneposílej obsah formuláře ani raw chybové texty. - Doporučené termíny používají dva privacy-safe Matomo eventy:
Rezervace / Doporučené termíny zobrazeny / service-slugse pošle jednou pro skutečně viditelnou konkrétní sadu aRezervace / Doporučený termín vybrán / YYYY-MM-DD | HH:mm–HH:mm | service-slug | pozice Npouze při skutečném kliknutí klientky na doporučenou kartu, nikdy jen proto, že se čas vybraný v kalendáři shoduje s canonicalkeydoporučení. Mobilní zobrazení zahrnuje první čtyři karty, desktop až šest; impression a pozice proto vždy vycházejí z aktuálně viditelné sady. Podíl eventových výběrů doporučení jeDoporučený termín vybrán / Čas vybrán; eventové CTR doporučení jeDoporučený termín vybrán / Doporučené termíny zobrazeny. Nejde automaticky o podíl unikátních klientek ani sessions. Služba předvyplněnaje čistě diagnostický event pro vstup z ceníku/detailu služby a nesmí zároveň znovu zapisovatSlužba vybrána; funnel krokservicemá reprezentovat skutečné ruční potvrzení služby ve formuláři. Stejně takDatum vybránov booking flow posílej jen při změně konkrétníhodateKey, ne znovu při kliknutí na čas ve stejném dni.- Pro tokenové self-service route drž stejné bezpečnostní pravidlo: lze měřit jen neosobní lifecycle kroky jako
Správa rezervace / Změna termínu otevřena,Správa rezervace / Datum vybráno,Správa rezervace / Čas vybrán,Správa rezervace / Změna termínu odeslána,Rezervace / Storno odeslánoaRezervace / Storno dokončeno, ale nikdy ne token, plnou URL, jméno klientky ani kontakt. Privacy regex vsrc/features/analytics/matomo.tsnesmí blokovat samotná business slova typustorno; filtrovat má jen PII a raw tokenové cesty. - E2E krytí pro tento guard je v
tests/e2e/booking-flows.spec.ts: Playwright stubujematomo.js, sbírá_paqvolání a ověřuje, že/rezervace/storno/[token]pouze inicializuje safe URL beztrackPageView, ale po potvrzení storna odešleStorno odeslánoaStorno dokončenobez raw tokenu. - Pokud
/rezervacevstupuje s validním query prefillservice=..., posílej vedle standardníhoSlužba vybránai samostatný eventSlužba předvyplněna; tím zůstane starý funnel kompatibilní, ale Matomo reporty umí oddělit CTA z ceníku/detailu služby od ruční volby až v booking flow. - API endpoint
src/app/api/admin/analytics/route.tsvrací tuto agregaci jako JSON. Route je chráněná přesgetSession()a pustí jen roleOWNER/SALON; bez session vrací403, při interní chybě vrací200s bezpečným nulovým fallbackem a em dashtopSource. src/components/admin/AnalyticsWidget.tsxje klientská komponenta pro admin dashboard. Na mountu voláfetch('/api/admin/analytics'), validuje shape payloadu v runtime, zobrazujeNačítání…/Data nejsou dostupnáa po úspěchu vykreslí jen kompaktní souhrnVýkon webu(návštěvy dnes,rezervace dnes,míra rezervace). Detail zdrojů a funnel kroků je dostupný až v rozbaleníZobrazit analytiku, aby hlavní cockpit neukazoval matoucí procenta jako primární provozní signál.DashboardPageje kompaktní denní provozní cockpit. Priorita stránky jeProvozní přehled -> Vyžaduje pozornost -> provozní KPI -> Dnešní plán / Nejbližší volné termíny -> Rychlé akce / Tento týden / Výkon webu; horní provozní blok má fungovat jako nízká operační lišta, KPI jako jeden metric strip s krátkým detailem metriky a pravý sloupec jako krátký podpůrný panel. Disabled stavMatomo není nakonfigurované.se rozhoduje na serveru podle přítomnostiMATOMO_URL,MATOMO_SITE_IDaMATOMO_AUTH_TOKEN; klient kvůli tomu nečte žádný secret.- Sekce dashboardu
Vyžaduje pozornostse renderuje pouze při existenci actionable alertů (alert.emphasis !== "ok"); pokud jsou jenokstavy, komponenta vracínull. Komponenta musí použítalert.emphasis:primaryalert je vizuálně hlavní, sekundární alerty jsou menší podpůrné položky. Pokud primární alert neexistuje, jako hlavní se použije první actionable alert. - Když se sekce
Vyžaduje pozornostzobrazí, musí být mobilně odolná: text alertu se zalamuje (break-words) a CTA může být pod textem; nepoužívej zdetruncatev řádku s CTA, protože to způsobuje horizontální přetečení karty na úzkých iOS viewports. - Actionable alert pravidla v
getAdminDashboardData(...)jsou záměrně provozní a konzervativní:pending-bookings,email-failuresacurrent-overdue(aktivní rezervace po konci termínu). Samotná nulová dostupnost dnes/zítra nebo nízká týdenní kapacita se v této sekci neeskalují jako problém. - Rychlé akce v dashboardu neduplikují horní primární CTA
Vytvořit rezervaci; data read modelu drží podpůrné vstupybookings,availability,clients,vouchers. Pokud se sem přidá další akce, musí být provozně podpůrná a nesmí soutěžit s hlavní CTA v hero liště. - Admin sekce
Rezervacepo density refaktoru používá jeden horní control panel: quick stats (statquery param) a formulářové filtryquery/status/source/dateFrom/dateTosdílejí stejnou kartu a dál zůstávají plně URL-driven.sourcefiltr je kanál vytvoření rezervace, ne UTM/referrer akvizice. Na mobilu panel není sticky (kvůli překryvu seznamu), sticky zůstává odmdvýš. - Stejná sekce nově drží i URL-driven progresivní odkrývání seznamu:
showPast=1rozbalí historickou skupinu aneedsClosureLimit/pendingLimit/upcomingLimit/pastLimitřídí, kolik položek je vidět v jednotlivých blocích. Pokud upravuješ toolbar nebo odkazyZobrazit další, zachovej tyto query parametry i při další filtraci. - Search input v rezervacích používá malý klientský combobox s debounce
POSTfetch do/api/admin/bookings/search; hledaný text patří výhradně do JSON body, nikdy do URL. Endpoint musí zůstat admin-only, same-origin a vracet bezpečné textové návrhy bez přímé mutace URL nebo DB. Pro běžný text nabídne hlavněfullNamea název služby; kontaktní návrhy (e-mail/telefon) se mají ukázat jen když dotaz vypadá jako kontakt. Odpověď je vždyCache-Control: private, no-store. - Mobilní layout tohoto control panelu je citlivý na implicitní minimální šířku CSS grid items a nativních
input[type="date"]. Labely, inputy/selecty i akční řádek proto držmin-w-0; nepřidávej pevné mobilní šířky, které by znovu vytvořily horizontální přetok v admin shellu. - Veřejný
SiteHeaderpřepíná na desktop navigaci až odlg;mdviewporty (včetně iPad portrait) mají zůstat v kompaktním režimu s mřížkovou navigací a mobilním CTA, aby nedocházelo k useknutí pravého desktop tlačítka. - Rezervační KPI strip se počítá serverově v
getReservationsData(...)bez nového persistence modelu:Čeká na potvrzení:Booking.status = PENDINGDnes: všechny rezervace sescheduledStartsAtv dnešním dniTento týden: všechny rezervace v aktuálním týdnu počítaném od pondělíBez kontaktu: rezervace, kde jeclientEmailSnapshot = ""a zároveňclientPhoneSnapshotprázdný nebonull
- Read model seznamu rezervací už nepoužívá
Dnes / Zítra / Později / Dříve; seskupení jeneeds_closure / pending / upcoming / past.needs_closurevítězí jako první a používáscheduledEndsAt < nowplus aktivní stavyPENDING/CONFIRMED, aby proběhlé návštěvy čekající na uzavření nezapadly mezi minulými rezervacemi.pendingpak drží budoucí čekající rezervace,upcomingostatní aktivní budoucí rezervace apasthistorické i uzavřené stavy (COMPLETED,CANCELLED,NO_SHOW). - Souhrnný počet výsledků v rezervacích se nesmí odvozovat od viditelného výřezu nebo interního
findManylimitu. Read model má počítatsummary.totalCountsamostatně přescount(where)a teprve potom nad celým výsledkem aplikovat skupinové limity pro UI. - Admin login
/admin/prihlasenimá zůstat krátký a netechnický: neukazuj interní role, session ani bootstrap účty v copy/placeholderu a zachovej výraznýfocus-visiblestav polí i submit tlačítka. - Dashboard analytics API vrací také
periodLabelasources. Backend čteReferrers.getCampaignspro kampaně a při absenci kampaní používáReferrers.getReferrerType; labely mapuje na business názvyInstagram,Firmy,Google,Přímý vstup, případněOffline/Ostatní. Rezervace u zdrojů jsou odhad: početRezervace / Vytvořenase rozdělí podle podílu návštěv jednotlivých zdrojů a UI je proto označuje jako odhad, ne přesnou atribuci. /api/admin/analyticsteď vrací ireportingStatusa volitelněreportingMessage, aby frontend poznal rozdíl mezi reálnou nulou a problémem v Matomo reportingu. Lokální CLI kontrola používánpm run analytics:check; sahá jen na serverovéMATOMO_*env a nevystavuje token klientovi.- Owner Pushover notifikace maji Next.js server-only wrapper v
src/lib/notifications/pushover.tsa sdilenou worker-safe implementaci vsrc/lib/notifications/pushover-core.ts. Verejne API jesendOwnerPushover(input), plus interni helpery pro booking a email failure zpravy; vsechny chyby se catchuji a loguji, aby Pushover nikdy nerozbil transakci rezervace, doruceni emailu ani reminder scan. - V admin read modelu e-mail logu drzi
recentEmails[].trackingStateValuestejny union kontrakt jakoderiveTrackingState(...), tedy vcetneprocessing; pri doplneni noveho tracking stavu aktualizuj vzdy obe strany najednou, jinaknext buildspadne na nekompatibilnich typech. - Pushover HTTP volani jsou navic omezena 3s timeoutem pres
AbortSignal.timeout(...), aby pomala externi API nezdrzovala user-facing booking server actions a CI E2E scenare. - Pokud Pushover vola kod nacitany standalone skriptem (
email:worker, jednorazove CLI), importuj@/lib/notifications/pushover-core, protozeimport "server-only"je urceny pro Next.js bundler a v plain Node procesu by shodil start. - Per-user nastaveni drzi model
UserNotificationSettingss vazbou 1:1 naAdminUser. Dotazy na prijemce vzdy filtrujiAdminRole.OWNER,isActive = true,pushoverEnabled = true, vyplnenypushoverUserKeya zapnuty konkretni event toggle;SALONse nema nikdy objevit ani v UI, ani v odesilacim dotazu. - Obecný Pushover rate limit zůstává jednoduchý in-memory guard v procesu: kombinace
type + bookingId/contextId/emailLogIdse smí odeslat maximálně jednou za 30 sekund. Výjimkou je veřejný bookingPUBLIC_BOOKING_RATE_LIMITED, který používá databázový atomický cooldown 10 minut podle bezpečnéhosourceHasha funguje i po restartu nebo mezi instancemi. - Pushover volani patri pouze do dulezitych side-effect bodu po uspesne domenove zmene:
createBookingWithEngine, admin/email potvrzeni nebo zruseni, public storno, centralnirescheduleBooking, finalni selhanideliverEmailLog, selhani enqueue reminderu a vybrane neocekavane systemove chyby. Nepouzij ho pro pageviews, Matomo eventy, admin kliky ani bezne odeslani emailu. - U
NEW_BOOKINGPushover zpravy doplnKlientka: Nova klientkaneboKlientka: Vracejici se klientkapodle existence rezervace stejneclientIdpred aktualni rezervaci. Neodvozuj tento udaj jen z aktualniho jmena, e-mailu ani telefonu a nepridavej do zpravy kontakt klientky. - Pro neocekavane serverove pady preferuj shared helper
sendOwnerSystemErrorPushover(...)misto ad-hoc skladaniSYSTEM_ERRORpayloadu. Helper drzi jednotny typ alertu, pridava zkraceny summaryError.messagea zachovava stavajici 30s rate-limit podlecontextId. - Aktualni provozni scope
SYSTEM_ERRORalertu zahrnuje verejne i admin booking flow (create/reschedule), public booking schema drift, fail enqueue navazneho reschedule emailu, planner mutace volnych terminu, voucher create/update/cancel + voucher email queue, owner invite resend flow a vybrane admin/API endpointy typuhealthneboanalytics. Naopak validacni chyby, business konflikty a cizi neplatne requesty se notifikovat nemaji. - Booking e-maily v
src/lib/email/templates.tssdílejí email design systém: 600px shell, inline styly, prezentační tabulky, systémové fonty pro CTA, jednotné blokySlužba / Datum / Čas,Místo,Kontakta tlumenou patičku. Další šablony napojuj na tyto helpery místo ručního skládání vlastního layoutu. - Klientské booking e-maily berou salon kontakt z
getPublicSalonProfile()agetEmailBrandingSettings(): název, adresa, telefon a e-mail jdou primárně zeSiteSettings, mapový odkaz se skládá přes Google Maps query z aktuálního názvu a adresy. Pevné údaje PP Studia (PP Studio,Sadová 2, 760 01 Zlín,info@ppstudio.cz,+420 732 856 036) smějí zůstat jen jako fallback při chybějícím nastavení nebo DB chybě. - Kontakt ani pomocné věty v klientských booking e-mailech neduplikuj v intro, CTA ani patičce. Text/plain varianta musí zůstat plnohodnotná a všechny uživatelské hodnoty v HTML vždy escapuj přes lokální helper, zejména jméno klientky, službu, voucher kód a klientskou poznámku v admin e-mailu.
- Lokální náhledy booking šablon generuje
npm run email:previews; výstup je vtmp/email-previewsa skript nepřidává žádnou novou knihovnu ani nezapisuje doEmailLog. - Klientský e-mail
booking-approved-v1se renderuje vsrc/lib/email/templates.tsa má zůstat krátký, email-safe a mobilně čitelný: potvrzení rezervace, termín, služba, viditelná adresa, připomenutí.icspřílohy, jednorázový kontakt na studio a sekundární odkazy na správu rezervace dole. - Veřejně dostupná nahraná média se servírují přes route handler
src/app/media/[kind]/[[...path]]/route.ts, ne přespublic/repozitáře. next.config.tspoužíváallowedDevOriginspro lokální LAN vývoj na192.168.0.143i pro public dev test přesppstudio.cz/www.ppstudio.cz; bez toho Next.js 16 z jiného zařízení nebo přes reverse proxy zablokuje dev assety a HMR endpoint/_next/webpack-hmr.npm testanpm run test:db:bookingběží snode --import ./src/test/register-server-only.mjs --import tsx --test ..., takže plain Node test runner umí načístimport "server-only"bez zásahu do ostatních Next internals. Pokud přidáš další server-only moduly, použij tenhle sdílený hook místo lokálních per-test stubů.- Veřejné vytvoření rezervace (
createBookingWithEngine) používá retry smyčku pro PrismaP2034serializační konflikty. Aktuálně je limitMAX_BOOKING_TRANSACTION_RETRIES = 5a mezi pokusy je krátký lineární backoff, aby se snížila flakiness při paralelních DB testech i v produkční špičce. - V Prisma 7 s
@prisma/adapter-pgmůže stejný serializační konflikt probublat jakoDriverAdapterErrorscause.kind = "TransactionWriteConflict"místoPrismaClientKnownRequestError(P2034). Retry helpery v booking create/reschedule/cancel flow musí brát oba tvary jako retryable. - Kontaktní krok veřejné rezervace (
booking-flow/contact-step.tsx) musí zachovat explicitníid/htmlForvazby, stabilní ID pro hint/error texty aaria-describedbyna všech polích; chybové texty oznamuj přesaria-live="polite"neborole="alert"a klávesnicovýfocus-visiblestav drž v akcentu PP Studia. - Běžné platby k rezervaci jsou samostatný ledger
BookingPayment; nepřepisuj jím voucher redemption model ani individuální slevu. Stav úhrady se neukládá do DB, počítá hogetBookingPaymentSummary(...)z efektivní ceny rezervace (Booking.finalPriceCzk ?? Booking.servicePriceFromCzk),VoucherRedemption.amountCzkaBookingPayment.amountCzk. Server actions pro zápis/mazání plateb jsou vsrc/features/booking/payments/actions/booking-payment-actions.tsa UI patří přímo do existující sekceÚhradav detailu rezervace.SERVICEvoucher je nárok na službu, takže jeho čerpání nemá přebírat individuální cenu rezervace jako hodnotovou slevu. - Voucher doména je v
src/features/vouchersa zůstává oddělená od admin UI, PDF i public booking flow. Entry body:lib/voucher-code.tsgeneruje a normalizuje kódyPP-YYYY-XXXXXX.lib/voucher-validation.tsvrací bezpečný public validační výsledek bez citlivých polí;verifyVoucherPublic(...)je určený pro samostatnou veřejnou kontrolu bez vazby na službu a bez jakéhokoli uplatnění.lib/voucher-redemption.tsprovádí admin uplatnění v transakci, zapisujeVoucherRedemptiona blokuje další voucher na rezervaci, která už má alespoň jedno čerpání.lib/voucher-management.tsdrží server-side doménové funkce pro vytvoření, validaci a uplatnění bez klientských komponent.
- Voucher doménové moduly nesmí mít top-level
"use server". Veřejně volatelné server actions patří dosrc/features/admin/actions/*, kde každá action znovu ověřuje session a roli, než zavolá voucher doménu. CreateVoucherInputaRedeemVoucherInputjsou inferované ze Zod schémat s transformacemioptionalText, takže v testech je potřeba explicitně předávat i klíče, které mají být prázdné (undefined) - typickypurchaserName,recipientName,message,internalNoteanote.- Efektivní expirace voucheru je aplikační read pravidlo přes
getEffectiveVoucherStatus(...); validace ani read modely automaticky nepřepisují DB status naEXPIRED. - Admin seznam voucherů je základní přehledová UI vrstva nad voucher doménou:
- route factory obsluhuje
/admin/voucheryproOWNERi/admin/provoz/voucheryproSALON, - navigace a guard berou
voucheryjako sdílenou admin sekci, - stránka používá
src/features/admin/lib/admin-vouchers.tsjako read model asrc/features/admin/components/admin-vouchers-page.tsxjako prezentační vrstvu, - desktop používá skutečnou kompaktní tabulku jako primární fokus a menší šířky přecházejí na voucher karty,
- create CTA je v nízké page header liště vpravo, metriky jsou v nízkém stripu a filtry
q,type,statuszůstávají URL-driven, - KPI strip je serverový read-model souhrn:
Zbývá k uplatněnísčítá jen otevřenéVALUEzůstatky a počet otevřenýchSERVICEvoucherů,Brzy expirujísleduje otevřené vouchery svalidUntildo 30 dnů, - badge stavů musí být vizuálně součástí řádku ve sloupci
Stav; nevyváděj je mimo tabulku ani z nich nedělej druhotný seznam, - query parametry jsou
q,typeastatus; filtr stavu musí odpovídat efektivnímu voucher statusu, ne jen hodnotě uložené v DB.
- route factory obsluhuje
- Statické voucher routy
/admin/vouchery/*a/admin/provoz/vouchery/*mají vlastnílayout.tsxexportujícíAdminShellLayout; nové statické admin routy mimo dynamický[section]wrapper musí dostat stejný layout, jinak se vykreslí mimo tmavý admin shell. - Detail voucheru běží přes konkrétní routy
/admin/vouchery/[voucherId]a/admin/provoz/vouchery/[voucherId], route factorycreateAdminVoucherDetailRoute(...), admin wrappergetAdminVoucherDetailData(...)a komponentusrc/features/admin/components/admin-voucher-detail-page.tsx. Prezentační vrstva má být provozní workspace: jedna summary karta nahoře, pod ní layoutParametry voucheru + Historie uplatněnívlevo aKupující a odeslání + Poslední e-mailové pokusyvpravo. Nepřidávej další vysoké samostatné karty a nerozšiřuj kvůli detailu klientskou hranici celé stránky. - Provozní editace voucheru je oddělená v panelu
src/features/admin/components/admin-voucher-operations-panel.tsxa server actionssrc/features/admin/actions/voucher-actions.ts. Smí měnit jen bezpečné provozní údajepurchaserName,purchaserEmail,validUntilainternalNote; kód, typ, hodnota, služba, měna, čerpání a PDF identita zůstávají neměnné. - Voucher se ruší přes stav
VoucherStatus.CANCELLED,cancelledAt,cancelledByUserId,cancelReasonaupdatedByUserId; záznam se nemaže. Zrušení je povolené jen pro voucher bezVoucherRedemption,OWNERiSALONmají pro tyto provozní voucher akce stejná práva a veřejné ověření nesmí ukázat interní důvod zrušení. - Detail voucheru má navíc read-only sekci
Odeslání e-mailem, která zEmailLogtypuVOUCHER_SENTvrací jen bezpečnou historii posledních 5 záznamů. Read model nesmí vracet raw payload,processingToken,providerMessageIdani jiná technická pole; pokud dotaz na historii selže, detail voucheru má dál fungovat a jen zobrazit prázdný stav. - PDF voucheru běží přes route handlery
/admin/vouchery/[voucherId]/pdfa/admin/provoz/vouchery/[voucherId]/pdf, sdílený handlercreateAdminVoucherPdfRoute(...), Next.js wrappersrc/features/vouchers/lib/voucher-pdf.tsseimport "server-only"a worker-safe coresrc/features/vouchers/lib/voucher-pdf-core.ts. Handler vždy ověřuje session a roliOWNERneboSALON, PDF neukládá do DB ani na disk a generuje ho z aktuálního read modelu voucheru. - Voucherová šablona je po bootstrapu výhradně perzistentní záznam
VoucherTemplates privátním masterem vMEDIA_STORAGE_ROOT; každý voucher ukládá přesnýtemplateKeyitemplateIda renderer historického voucheru nefallbackuje na aktuální default. Seedclassic-v1.pdfje neveřejný bootstrap asset vsrc/features/vouchers/bootstrap-assets/, který je explicitně zahrnutý do Next output-file trace. Nový design znamená nový key (classic-v2,christmas-v1), nikoli změnu geometrieclassic-v1; skutečný runtime master se nikdy nenačítá zpublic/. classic-v2.pdfje připravený ColorPoint/Fujifilm Revoria uncoated kandidát s embedded profilem z oficiálního ColorPoint URL; jeho finální schválení tiskárnou, instalace do persistentníVoucherTemplatea přepnutí defaultu jsou samostatné rollout kroky. Reprodukční příprava používánpm run voucher:templates:prepare-classic-v2 -- classic-v1.pdf profil.icc classic-v2.pdfa profil se do repozitáře neukládá samostatně.- Každá template definition nese vlastní
layout, včetně page size, TrimBoxu, dynamických oblastí, souřadnic QR a typografických parametrů. Renderer je obecný a pouze řešítemplateKey -> registry -> master + layout; neobsahuje větvení pro konkrétní designy. DIGITAL a PRINT používají stejný layout, DIGITAL je pouze vektorový crop PRINT stránky. Nová šablona musí projít úplným checklistem výše včetně trusted asset readeru, registry, layoutu a regresních/preflight testů; formulář vytvoření i Nastavení → Vouchery ji pak načtou automaticky z aktivních definition.allowedTypesfiltruje UI a je znovu vynuceno serverem. - Preview šablon je lokální asset z registry, nikoli Media Manager; chybějící PNG zobrazí v admin formuláři náhradní stav a neblokuje voucherový backend ani PDF generování.
- Sdílený worker-safe core v
src/features/vouchers/lib/voucher-pdf-core.tsproclassic-v1personalizuje master PRINT stránku 216 × 105 mm s bleedem a TrimBoxem 210 × 99 mm na offsetu 3 mm. Tyto hodnoty jsou uloženy v layoutuclassic-v1a exportované konstanty v core jsou pouze zpětně kompatibilní aliasy. DIGITAL vzniká vektorovým výřezem stejné PRINT stránky; routy/pdfa/pdf/tiskzachovávají původní URL kontrakt. - PDF generátor používá
pdf-lib,qrcode,@pdf-lib/fontkita Noto Sans z@fontsource/noto-sans, aby bezpečně fungovala česká diakritika bez commitování fontů do repozitáře. QR kód míří na veřejnou URL/vouchery/overeni?code=...nadsiteConfig.url. SiteSettings.voucherPdfLogoMediaIdzůstává v DB kvůli zpětné kompatibilitě, ale aktuální admin UI ani voucherový PDF/email generator ho nepoužívají. Grafika voucheru včetně loga a pevných kontaktních údajů je součástí verzovaného master PDF a PDF šablony se do Media Manageru nepřidávají.- Voucherový PDF renderer nečte kontaktní údaje ze
SiteSettingsani zVOUCHER_PUBLIC_DOMAIN/NEXT_PUBLIC_SITE_DOMAIN: při načtení masteru ověří jeho rozměr podle registry a poté doplní pouze hodnotu nebo název služby, platnost, kód voucheru a QR kód. Pevná grafika, logo, adresa, web a ostatní texty jsou součástí masteru. Kontakty pro web a e-maily nadále používajíSiteSettings. - Tvorba voucheru běží přes konkrétní routy
/admin/vouchery/novya/admin/provoz/vouchery/novy, route factorycreateAdminVoucherCreateRoute(...), sdílený formulářsrc/features/admin/components/admin-voucher-form.tsxa server actioncreateAdminVoucherAction(...). Admin action vždy ověřuje roliOWNERneboSALON, znovu validuje Zod schéma a proSERVICEpovolí jen aktivní službu. Formulář předvyberetemplateKey,validFromnastaví na dnešek avalidUntilpočítá podleSiteSettings.voucherDefaultValidityMonths; při jediné aktivní šabloně zobrazí její preview bez zbytečného dropdownu. Prezentačně formulář v této verzi sbírá jen údaje o kupujícím; doménová polerecipientNameamessagezůstávají v backend kontraktu, ale admin UI je nepoužívá. - Ruční odeslání voucheru e-mailem je v detailu voucheru přes klientský panel
src/features/admin/components/admin-voucher-email-panel.tsxa server actionsrc/features/admin/actions/voucher-email-actions.ts. - Server action
sendVoucherEmailAction(...)vždy ověřuje session i roliOWNERneboSALON; samotný enqueue core je vqueueVoucherEmailLog(...)a validujevoucherId,recipientEmail,subjectpřes Zod. - Odeslání je povolené jen pro efektivní stavy
ACTIVEaPARTIALLY_REDEEMED(getEffectiveVoucherStatus(...));DRAFT,REDEEMED,EXPIRED,CANCELLEDvrací business chybu bez raw stack trace do UI. - Email outbox používá existující
EmailLogs novým enum typemEmailLogType.VOUCHER_SENTa template keyvoucher-sent-v1. VEMAIL_DELIVERY_MODE=backgroundse záznam frontuje pro worker + retry; vlogrežimu se zapisuje jako odeslaný log bez SMTP. - Šablona
voucher-sent-v1je napojená vsrc/lib/email/templates.tsa používá server-side helperbuildVoucherEmailTemplate(...)zsrc/features/vouchers/lib/voucher-email-template.ts. Příloha je DIGITAL PDF z worker-safegenerateVoucherDigitalPdf(...)vsrc/features/vouchers/lib/voucher-pdf-core.ts(bez HTTP callu na admin route), filenamevoucher-KOD.pdf, content typeapplication/pdf. - Obsah voucher e-mailu je v této verzi záměrně pevný (bez volného textarea vstupu v admin formuláři): pozdrav, věta
v příloze zasíláme dárkový poukaz PP Studio., detail poukazu, ověřovací URL, závěr a kontakt salonu včetně domény. - Voucher e-mail nikdy nesmí obsahovat
internalNote, historii čerpání, technická ID nebo booking metadata; posílá jen bezpečné business údaje + veřejný ověřovací odkaz/vouchery/overeni?code=.... - Admin uplatnění voucheru u rezervace běží v detailu rezervace přes
src/features/admin/components/admin-booking-voucher-form.tsxa server actionredeemBookingVoucherAction(...)vsrc/features/admin/actions/booking-actions.ts. Action vždy ověřuje roliOWNERneboSALON, volá výhradně doménovéredeemVoucherForBooking(...), vrací bezpečné hlášky a revaliduje owner i salon booking/voucher přehledy. Jedna rezervace smí mít nejvýše jeden skutečně uplatněný voucher; další pokus se zastaví na doménové vrstvě. - Pokud detail rezervace už zobrazuje intended voucher z veřejného flow, formulář
AdminBookingVoucherFormse renderuje přímo uvnitř stejné voucher karty. Nepoužívej klientský anchor skok na formulář níž v panelu; v praxi to působilo jako nečekané odskočení viewportu bez zjevné změny stavu. - Panel
Úhradav admin detailu rezervace kombinuje individuální cenu rezervace, settlement summary, voucherové čerpání a běžné platby mimo voucher.getAdminBookingDetailData(...)vracívoucher.paymentSummary:totalPriceCzkbere z efektivní cenyBooking.finalPriceCzk ?? Booking.servicePriceFromCzk, fallbackově z aktuálníService.priceFromCzk;voucherPaidCzkje součetVoucherRedemption.amountCzk;directPaidCzkje součetBookingPayment.amountCzk;paidAmountCzk/paidTotalCzkpočítá helpergetBookingPaymentSummary(...);remainingAmountCzkje doplatek po voucheru i běžných platbách.Booking.finalPriceCzkje provozní obchodní úprava ceny se zdůvodněním proOWNERiSALON, ne záporná platba. ProVALUEvoucher se doporučená částka odvozuje z doplatku po finální ceně;SERVICEvoucher částku nezadává a doménově řeší jen shodu služby. - Vizuální priorita panelu
Další krokjeaktuální stav -> hlavní provozní CTA -> sekundární provozní akce -> nebezpečná akce. UCONFIRMEDrezervace musí býtDokončit návštěvunejvýraznější akce,Přesunout termínaNedorazilasekundární aZrušit rezervacioddělené v samostatné danger sekci s důvodem zrušení. Nemíchej storno do běžného chooseru vedle dokončení návštěvy. - Aktuální completion flow používá CTA
Dokončit návštěvu. Pokud je doplatek> 0, akce má vést přes kompaktní volbu režimu úhrady (Hotově,QR,Voucher,Kombinovaně,Bez platby) a teprve potom uzavřít status naCOMPLETED. Bez platbynesmí být tichá zkratka: v completion flow vyžaduje povinný důvod a do historie se zapisuje explicitní informace, že rezervace byla dokončená s neuhrazeným doplatkem.- Server action completion flow je
completeBookingVisitAction(...)vsrc/features/admin/actions/booking-actions.ts; při completion může zapsatBookingPayment, uplatnit voucher přes existující doménuredeemVoucherForBooking(...)a následně provést status transition přesapplyAdminBookingStatusChange(...). - Před zápisem platby/voucheru musí completion flow znovu ověřit
canCompleteBookingAt(...)a plánovanou úhradu proti aktuálnímu doplatku. Pokud hotovost/QR/voucher/kombinace nepokryje celý doplatek, action vrátí chybu a admin musí doplnit úhradu nebo vědomě použítBez platbys důvodem. - Completion panel pro režim
VoucheraKombinovaněmá pomocné načtení voucheru přes APIPOST /api/admin/vouchers/lookup; po zadání kódu vrací stav voucheru a uVALUEpředvyplní doporučenou částku podle zůstatku voucheru a aktuálního doplatku. - Klientský lookup ve completion panelu musí být odolný proti race condition: starší requesty se abortují a stale odpovědi se ignorují, aby úspěšně načtený voucher nepřepsala pozdější chybová hláška.
- Lookup request ve completion panelu používá
credentials: same-origina má jeden automatický retry při síťové chybě; fail větev loguje chybu do konzole s kódem voucheru a request id pro snadnější provozní diagnostiku. - Lookup endpoint přijímá výhradně
POST /api/admin/vouchers/lookups kódem voucheru v JSON body; completion panel používácache: no-storea endpoint vracíCache-Control: private, no-store. - Pro kompaktní provozní variantu detailu drž panel
Další kroknízký: krátký status řádek bez duplicit, kompaktní akční tlačítka v jedné mřížce a potvrzení vybrané akce jako jeden řádek (vysvětlení + důvod + potvrdit) bez velké opakující se preview karty. - Sekundární karta
Přesunout termínv paneluDalší krokmusí být shrink-safe: wrapper grid položky i success banner po uložení mají dovolitmin-w-0a zalamování textu, aby dlouhé labely termínu nebo warning copy nerozbily layout completion flow. - Sekce
Nebezpečné akcemá být výchozně sbalená (Nebezpečné akce+Rozbalit), abyZrušit rezervacinebyla dominantní při běžném otevření detailu. - Vizuální priorita panelu
Úhradaje záměrněStav úhrady -> Cena k úhradě -> doplatek -> + Zapsat platbu -> + Uplatnit voucher -> Přehled úhrad. HorníPaymentSummaryBlockje dominantní a obsahuje i kompaktní vstup do individuální úpravy ceny přesUpravitu položkyCena k úhradě; samostatný viditelnýBookingPriceBlockse ve výchozím zobrazení nepoužívá. Doplatek nebo přeplatek má být nejsilnější finanční hodnota.+ Zapsat platbudrž dobře viditelné, ale ne silnější než hlavní CTA v paneluDalší krok; existující běžné platby nevypisuj ve zvláštním bloku mimo ledger, patří jednou doPřehled úhradspolu s voucher redemptions a případným smazáním platby. Voucher akci drž jako sekundární/outline přímo v kompaktním voucher bloku a prázdnýPřehled úhradvizuálně tlumený. - V kompaktním detailu může být platební ledger pod rozbalením
Detail úhrady, ale výpočty a data (paidTotalCzk,voucherPaidCzk,directPaidCzk,remainingAmountCzk) se nesmí měnit. - Stav úhrady je jen odvozený read model, ne uložený DB stav:
UNPAID,PARTIALLY_PAID,PAIDneboOVERPAID.BookingPaymentje ledger přijatých plateb mimo voucher (CASH,CARD,BANK_TRANSFER,OTHER); voucherové čerpání dál zůstává výhradně veVoucherRedemption.OWNERiSALONsmí platbu zapsat, mazání platby je omezené naOWNER. - U hodnotového voucheru s nižším zůstatkem než cena služby admin formulář předvyplňuje maximální použitelnou částku a vysvětluje doplatek mimo voucher. Serverová doména je autoritativní: pokud zadaná částka převyšuje
remainingValueCzk, uplatní pouze dostupný zůstatek voucheru. Server action po částečné úhradě vrací success hlášku s uplatněnou částkou a dopočteným doplatkem vůči zadané částce. - Read model
getAdminBookingDetailData(...)smí pro sekciDárkový poukazv paneluÚhradavracet jen provozně bezpečná pole: intended voucher kód, typ, efektivní stav, bezpečný popis, doporučenou částku a historii redemptionů s aktérem. Technická ID nemají být primárním UI údajem. - Detail voucheru smí zobrazovat interní poznámku pouze v adminu; veřejné PDF ani veřejné ověření voucheru ji nesmí číst z veřejného read modelu.
- Veřejné ověření voucheru běží na
/vouchery/overeni?code=..., mánoindexmetadata a záměrně není vsitemap.ts. PoužíváverifyVoucherPublic(...), smí zobrazit jen kód, typ, zbývající hodnotu uVALUE, název služby ze snapshotu uSERVICEa platnost do; pro CTA může interně předat i veřejný slug navázané služby, nikdy však technické ID ani citlivá data. Neplatným kódům vrací pouze bezpečné důvody. Následné CTA po platném výsledku může vést na rezervaci nebo veřejný kontakt studia, ale nesmí spouštět uplatnění ani zapisovat voucher data. - Veřejná prezentační route
/voucheryje indexovatelná landing page pro akvizici a rozhodnutí před nákupem voucheru. Patří dositemap.ts, používá oddělený metadata helperpublic-page-metadata.ts, vlastní komponentuvoucher-landing-page.tsx, pro blok doporučených služeb má číst jen úzký výběr přesgetVoucherSuggestedServices(3)místo celého katalogu a má vést jen na bezpečné veřejné kroky (kontakt,mailto:,tel:,/vouchery/overeni, případně detail služby), ne na admin nebo přímé čerpání voucheru. - Stejný princip drž i pro veřejné route
/faqa/kontakt: pokud není potřeba sdílet celý page modul napříč webem, preferuj route-specific komponenty (faq-page.tsx,contact-page.tsx) a lehký metadata helper před importem celéhopublic-site.tsx, aby cold compile v Next.js 16/Turbopack netahal zbytečný modulový strom. - Stejný princip drž i pro homepage
/a katalog/sluzby: preferuj route-specific komponenty (public-home-page.tsx,services-page.tsx) a metadata helper před importem celéhopublic-site.tsx, zvlášť když route potřebuje jen malou část veřejného UI a jinak by zbytečně natahovala další public stránky do stejného kompilovaného stromu. - Route
/vouchery/overenimá server-side anti-bruteforce guard vsrc/features/vouchers/lib/voucher-public-verification-rate-limit.ts: IP hash přesADMIN_SESSION_SECRET, okno 10 minut, limit 10 pokusů/IP a audit log doBookingSubmissionLogs prefixemPUBLIC_VOUCHER_VERIFY_*. - Route
/vouchery/overenije read-only: nesmí vytvářetVoucherRedemption, měnitremainingValueCzk, měnitVoucher.status, ukládat booking intent ani číst admin-only read model. - Veřejný submit
/rezervacemůže přijmout volitelnývoucherCode, ale nesmí odečítat zůstatek, měnit status voucheru ani vytvářetVoucherRedemption; smí pouze uložitintendedVoucherId,intendedVoucherCodeSnapshotaintendedVoucherValidatedAtnaBooking. VALUEvoucher se ve veřejném flow považuje za použitelný při kladnémremainingValueCzkbez ohledu na cenu služby; nižší zůstatek než cena není chyba a doplatek zůstává provozní záležitost při návštěvě.
- Každá veřejná stránka má vlastní route a metadata.
- Detail služby běží na
sluzby/[slug]a čerpá z request-time DB read modelu, aby admin změny byly vidět bez rebuildů. - V Next.js 16 mají dynamické route props asynchronní API; v
page.tsxagenerateMetadataproto typujparamsjakoPromise<...>a čti je přesawait params(jinak vzniká chybasync-dynamic-apis). - Veřejný web drží dva zdroje obsahu:
- marketingové bloky, FAQ a právní texty jsou dál centralizované v
src/content/public-site.ts - služby a ceník berou data z DB přes
src/features/public/lib/public-services.ts
- marketingové bloky, FAQ a právní texty jsou dál centralizované v
- Krátké storno shrnutí mimo samostatné právní stránky navrhuj jako prezentační microcopy, ne jako procesní nebo interní termínologii: pro homepage trust metriku a FAQ preferuj formulace orientované na akci klientky (
upravit nebo zrušit termín,nejpozději 24 hodin předem) a drž je čitelné během 1-2 sekund. - FAQ už používá strukturovaný model
FaqSection -> FaqItem; při dalších úpravách preferuj tematické skupiny a krátké odpovědi před jedním plochým seznamem dlouhých textů. - FAQ stránka renderuje odpovědi serverově přes nativní
details/summary; nepřidávej klientskou state vrstvu, která by odpovědi vkládala do DOM až po kliknutí. FAQPageJSON-LD stavíbuildFaqPageJsonLd(...)ze stejného seznamu sekcí jako stránka. Při úpravě FAQ nejdřív změň viditelnýFaqItema až z něj nech vzniknout strukturovaná data, aby se schema nikdy nerozjelo s obsahem stránky.- Praktické FAQ má pokrývat i opakující se salonní dotazy kolem frekvence kosmetiky, příchodu s make-upem, citlivé pleti, úpravy obočí a výdrže barvení obočí; drž odpovědi konkrétní, ale bez medicínských slibů.
- FAQ průběžně rozšiřuj i o rozhodovací dotazy s organickým potenciálem: jak vybrat první službu, kdy zvolit lash lifting vs. laminaci obočí, jak dlouho tyto služby vydrží, kdy je lepší návštěvu kvůli podráždění očí odložit a kdy je vhodnější hodnotový voucher než voucher na konkrétní službu.
src/features/public/lib/public-services.tsnyní zároveň funguje jako thin read model nad rozšířeným katalogem:ServicenesepublicIntro,seoDescription,pricingShortDescription,pricingBadge; název služby je sjednocený vService.nameServiceCategorynesepricingDescription,pricingLayout,pricingIconKey,pricingSortOrder; veřejný název kategorie jde z aktuálníhoServiceCategory.name- fallbacky pořád existují, ale primární zdroj veřejné copy už je databáze, ne lokální slug mapy
- Ceník na
/cenikmá vlastní skladbu vsrc/features/public/components/pricing-page.tsx; obecnýpublic-site.tsxuž neobsahuje pricing-specific layout logiku. - Pricing modul je rozdělený na komponenty
PricingHero,CategoryChips,PricingSection,PricingItem,PricingGridSectionaPricingCTA, aby šlo věrně ladit spacing a hierarchii bez zásahu do ostatních veřejných stránek. /cenikuž nečte prezentační metadata z lokálních map; route používágetPublicPricingCatalog()a dostává z DB hotové kategorie včetně badge, icon key a layoutu.- Pořadí kategorií na
/cenikmá být konzistentní s/sluzbya/rezervace: priorita jeServiceCategory.sortOrder, až potompricingSortOrder. - Veřejné mapování kategorií je sdílené i pro booking katalog (
src/features/booking/lib/booking-public/catalog.ts) přes stejné poleServiceCategory.name, aby/rezervacepoužívala stejný label jako/sluzbya/cenik. - Public pricing read model (
getPublicPricingCatalog) validuje, že každá služba je v ceníku zařazená právě jednou kategorií; duplicita stejnéhoslugpřes více kategorií je tvrdá validační chyba. - Ceník už nepoužívá doprovodný blok s poznámkami.
- Úvodní stránka používá stejný DB katalog pro featured služby, aby odkazy z homepage mířily na aktuální slugs. Ruční výběr řídí
Service.isFeaturedOnHomepageahomepageSortOrder; public read model bere maximálně tři aktivní veřejné služby v aktivních kategoriích a při prázdném výběru padá zpět na katalogové pořadí. - Reusable page sekce jsou ve
src/features/public/components/public-site.tsx. - FAQ layout v
src/features/public/components/public-site.tsxje záměrně server-rendered bez další klientské state vrstvy; pro rozbalování odpovědí preferuje nativnídetails/summary, aby zůstal lehký a dobře kliknutelný i na mobilu. - Pravý box ve FAQ hero nemá opakovat kontaktní CTA z levé části; slouží jako klidný informační panel k první návštěvě s nanejvýš nenápadným anchor odkazem na
#prvni-navsteva. - Rychlá orientace FAQ používá anchor odkazy na sekce, globální smooth scroll a mobilní tap targety alespoň kolem 44 px; při dalších úpravách drž odkazy dostatečně velké i při delších českých názvech.
- U homepage hero drž jako preloadovaný LCP kandidát pouze logo (
next/imagepreload); portrait nemá mít stejnou prioritu, aby nebral bandwidth/render budget prvním pixelům loga. - Kontaktní stránka má vlastní modulární sekce v
src/features/public/components/contact-sections.tsx:ContactHeroContactMapPreviewCardContactParkingInfoCardQuickContactCardContactCardContactCTAContactMobileStickyCTA
- Kontakt data (
buildContactItems) drží i provozní mikrocopy a Google Maps deep-link pro adresu; odkaz pro adresu má mířit na konkrétní firemní profilKosmetika | Pavlína Pomykalová, zatímco iframe náhled může dál používat stabilní query podle adresy. Aktuální skladba stránky používá především kompaktní map preview, pravý quick contact panel a navazující full-width parkovací sekci. ContactParkingInfoCardna/kontaktpatří pod celou kontaktní mřížku, ne jen do levého sloupce. Aktuálně renderuje 4 nízké tip cards (Hradská,Gahurova,Sadová,Kongresové centrum Zlín) s orientační cenou pro návštěvu 90-120 minut, krátkou poznámkou a odkazemNavigovat.- U
Kongresové centrum Zlínpreferuj copy podle nejběžnějšího scénáře klientek: Po–Pá přes den. Pokud je karta určená hlavně pro pracovní dobu cca 8:00–18:00, komunikuj 70 Kč pro 90-120 minut jako běžnou orientaci a levnější večerní či víkendové režimy nech jen v drobném doplňku. - V parkovací sekci drž jen stručné doporučení podle ceny a pohodlí; nevracej se k dlouhé tabulce nebo plnému sazebníku. Další možnosti v centru (
Městské divadlo,Nad Tržnicí,Zlaté Jablko) smí být zmíněné nanejvýš v jedné doplňkové větě, ne jako hlavní karty. ContactHeromá při přítomnosti fotky studia držet dvousloupcovou skladbutext vlevo / obraz vpravo, na mobilu přirozeně padá pod text a hero obrázek nesmí přetékat mimo panel. Pro Next.js 16 používej u above-the-fold kontakt hero obrázkuloading="eager"a smysluplnésizes; nepřidávej zpět deprecatedpriority.- Stránka
/o-mneuž neběží jako jeden blok vpublic-site.tsx; vlastní skladba je vsrc/features/public/components/about-page.tsx. - Stránka
/o-mneje rozdělená do sekcíHeroSection,WhyChooseMeSection,StorySection,ApproachSection,WhatToExpectSectionaCertificationsSection, aby šlo pracovat s hierarchií bez monolitického JSX bloku. - Další vizuální ladění
/o-mnepreferuje jemný polish přímo v těchto sekcích místo dalšího přestavování IA; hlavní páky jsou proporce gridů, padding, typografická síla a optické vyvážení spodního okraje stránky. - U finálního polish passu preferuj drobné úpravy
max-width,gap,line-height,hoverashadowpřed zásahy do obsahu nebo dalšího členění sekcí. - Pokud stránka
O mněpotřebuje další micro tuning, drž se jen utility tříd v existujících komponentáchabout-page.tsxaabout-certificates-gallery.tsx. aboutContentvsrc/content/public-site.tspoužívá strukturovaný model (profile,whyChooseMe,story,approach,expectations,cta), aby bylo možné copy i CTA upravovat bez zásahu do layoutu;whyChooseMepodporuje krátký podnadpis sekce přesdescriptiona položkywhyChooseMe.itemsmohou mít vedle titulku idescriptionpro vysvětlující benefit text.- Copy na
/o-mnemá zůstat klidné, dospělé a osobní: neupozorňuj defenzivně na délku praxe, nepoužívej přehnané sliby ani superlativy a preferuj konkrétní popis přístupu, průběhu péče a přirozeného výsledku. - Veřejná klientská copy má reflektovat, že salon provozuje jedna osoba. Při úpravách textů v
src/content/public-site.ts,src/features/public/components/*a public route asides nepoužívej zbytečně týmové množné číslo typudoporučujemetam, kde má mluvit provozovatelka; přirozené společné formulace s klientkou (společně doladíme) a studio jako místo (k nám) jsou v pořádku. - Certifikace na
/o-mneberou public data zsrc/features/public/lib/public-certificates.ts, ale UI je záměrně připravené i na nulový stav pomocí placeholder karet vAboutCertificatesGallery. - Pro konzistentní vizuální rytmus napříč veřejnými stránkami drž hlavní obsah v jednotném wrapperu
Container(max-w-7xl) a vyhýbej se dalším globálním zúžením přesmx-auto max-w-*na úrovni celé sekce. - Pro konzistentní spacing preferuj na veřejných stránkách vertikální rytmus
py-10 / sm:py-14 / lg:py-16; větší rozestupy používej jen tam, kde mají jasný obsahový důvod (např. hero nebo výrazný CTA blok). src/components/layout/site-footer.tsxmá být kompaktní informační footer:- drž rozdělení na 3 desktop bloky
brand -> navigace/informace -> kontakt - uprostřed vždy zachovej dvě oddělené skupiny odkazů
NavigaceaInformace - kontakt má mít vyšší vizuální váhu než běžné linky, ale bez CTA nebo promo copy; obsahuje adresu, telefon a e-mail
- ve footeru zobrazuj e-mail i telefon v přirozeně čitelném tvaru; nepoužívej textový zápis
info [at] ... - footer i header udržuj server-rendered všude, kde to jde; kvůli trackingu nepřepínej celý shell na klientský rendering
- při dalších úpravách preferuj práci s
gap,max-width, typografickou vahou a jemným border/shadow rytmem místo přidávání dalších obsahových bloků
- drž rozdělení na 3 desktop bloky
- Veřejné e-mailové odkazy nepoužívej jako surový
mailto:přímo v SSR markupu; pro footer, kontaktní stránku a další veřejné kontaktní prvky používejsrc/components/ui/obfuscated-email-link.tsx, který:- má mít výchozí UI v plně čitelném tvaru s
@; textový zápislocal [at] domainpoužívej jen když je to výslovně zamýšlené konkrétním UI copy - skládá skutečný
mailto:až v klientu - umí i předvyplněný
subjectabodypro provozní e-mailové akce
- má mít výchozí UI v plně čitelném tvaru s
- Právní stránky ve
src/features/public/components/public-site.tsxmohou nově používat rozšířenou skladbuhero + aside + anchor TOC + sekce; pro právní texty proto preferuj strukturovaný obsah vsrc/content/public-site.tsmísto dlouhých monolitických odstavců. - Model
LegalSectionpodporujeid, seznamové body a volitelnou poznámku. U GDPR a podobných stránek tím drž kratší odstavce, lepší scanovatelnost a jasné anchor odkazy. - Placeholder obsah musí být jasně odlišen od finálních produkčních textů.
- Pokud je interní název služby příliš technický nebo exportovaný ze starého webu, uprav veřejné texty v adminu služby místo přidávání trvalého override v read modelu.
- Pro katalogová i veřejná metadata preferuj přímo pole v katalogu (
Service.publicIntro,Service.description,Service.pricingShortDescription,Service.seoTitle,Service.seoDescription,Service.idealFor,Service.includes,Service.benefits,Service.goodToKnow,ServiceCategory.pricingDescription). src/features/public/lib/service-copy-overrides.tsje pouze dočasný migrační/backfill zdroj podleslug; veřejný web ho nemá používat jako hlavní ani prioritní zdroj obsahu.- CTA na rezervaci držet konzistentně v headeru, hero sekcích a kontextových blocích.
- U homepage copy preferovat strukturu, která se už historicky osvědčila: jasný lokální hero claim, dvě primární akce (rezervace + ceník) a blok „nejste si jistá výběrem“, který snižuje bariéru první rezervace.
- Pokud homepage potřebuje logo/fotku majitelky, nastav to v
homepageContent(logoImage,portraitImage) a používej lokální soubory zpublic/brand, aby nebyla závislost na externím hostingu. - Stránka
/o-mneje výjimka z obecného pravidla „bez portrait-first kompozice“: může používat výraznější dvousloupcový hero s portrétem majitelky, pokud to pomáhá důvěře a rychlejšímu rozhodnutí klientky. - Statické soubory v
public/brandjsou vhodné jen pro ručně verzované assety projektu; admin uploady mají používat sdílenou media vrstvu a modelMediaAsset. - PWA manifest ikony odkazované v
src/app/manifest.webmanifestpřes root URL (např./android-chrome-192x192.png) musí fyzicky existovat vpublic/; běžný souborsrc/app/android-chrome-*.pngbez file-convention názvu metadata route není automaticky servírovaný na stejné URL. - Pokud se mění brand logo pro browser chrome, regeneruj společně
src/app/favicon.ico,src/app/apple-icon.pngipublic/apple-touch-icon*.png/public/android-chrome-*.png, aby zůstaly sladěné metadata route i manifest assets.
- Login probíhá přes
src/app/api/auth/login/route.ts. - Validace přihlašovacích údajů je server-side přes Zod.
POST /api/auth/loginmá server-side rate limit (10 minut, IP + e-mail hash) přes helpersrc/lib/auth/admin-login-rate-limit.ts.- Audit login pokusů (
SUCCESS,INVALID_PAYLOAD,INVALID_CREDENTIALS,RATE_LIMITED) se zapisuje doBookingSubmissionLogs prefixemADMIN_LOGIN_*. - Session payload je podepsaný JWT token v
httpOnlycookie. - Admin session cookie
ppstudio-admin-sessionmá idle expiraci 14 dní a používá sliding refresh vsrc/proxy.ts: pokud do expiry zbývá méně než 48 hodin, proxy vystaví novou cookie/JWT se stejným payloadem. - Session současně respektuje absolutní limit 45 dní od prvního přihlášení (
sessionStartedAtclaim); po překročení proxy cookie smaže a přesměruje na/admin/prihlaseni. - Hodnoty jsou konfigurovatelné přes env:
ADMIN_SESSION_IDLE_MAX_AGE_SECONDS,ADMIN_SESSION_REFRESH_WINDOW_SECONDS,ADMIN_SESSION_ABSOLUTE_MAX_AGE_SECONDS(sekundy). Guardrails: refresh window nesmí být větší než idle timeout a absolute timeout nesmí být menší než idle timeout. src/proxy.tsřeší rychlý auth gate pro admin: u/admin/*ověřuje podpis a expiraci session JWT cookie, ne jen její existenci; neplatnou cookie smaže a přesměruje na login.- Role-based autorizace se dokončuje uvnitř serverových helperů v
src/lib/auth/session.ts; po ověření JWT se admin session znovu načítá z DB, neaktivní uživatel session zneplatní a aktuální role se bere z DB. - Admin login přijímá výhradně aktivní databázové účty s
passwordHash. Recovery CLI obnoví DB OWNERa, zapíše auditní záznam a revokuje otevřené pozvánky; heslo se nepředává jako argument příkazu. - Owner approve/reject odkazy z e-mailu už smějí změnit stav rezervace pouze po aktivní admin session. Tokenová stránka může zobrazit akci, ale server action před mutací znovu volá admin auth helper.
- Pro role-based admin IA používáme dvě serverově chráněné oblasti:
OWNERna/admin/*SALONna/admin/provoz/*
- Neplatná nebo zakázaná admin sekce se neřeší jen skrytím v menu; routa se validuje server-side přes
src/features/admin/lib/admin-guards.ts. - Sekce
Přístupymá vlastní owner-only route workflow vsrc/features/admin/components/admin-users-page.tsx; už nepoužívá generický placeholder renderer zadmin-section-page.tsx.
- Unit testy běží přes Node test runner (
npm run test:unit); plný lokální preflight (npm test) na ně navazuje všemi DB integracemi. - Všechny DB integrační scénáře lze spustit cíleně a sériově přes
npm run test:db:integration; pouze booking podmnožinu přesnpm run test:db:booking. - Playwright E2E sada (
npm run test:e2e) má dvě vrstvy:booking-flows.spec.tsověřuje kritické rezervační a provozní workflow nad reálnými fixture daty.voucher-flows.spec.tsověřuje provozní lifecycle hodnotového voucheru přes browser: vytvoření v adminu, chráněné stažení PDF a otevření předvyplněného panelu pro ruční odeslání e-mailu. Samotné zařazení e-mail logu kryje integrační testvoucher-email-actions.integration.test.ts.site-smoke.spec.tsověřuje základní dostupnost veřejných rout, sitemap/robots kontrakt, bezpečné chybové stavy veřejných utility rout, auth redirect protected adminu a načtení hlavních OWNER/SALON sekcí bez plošného klikání každé akce.accessibility.spec.tsspouští axe nad reprezentativním veřejným webem, formulářem rezervace a přihlášeným admin dashboardem.
- Regrese ranního planner problému je krytá integračním testem
admin-slots/mutations.integration.test.ts: publikace konceptu přes den s existující rezervací musí rezervovaný interval zachovat a přepsat jen běžnou dostupnost před/po něm. - Browser E2E testy běží přes Playwright (
npm run test:e2e) v adresářitests/e2e. - Playwright spouští suite v projektech
chromium,mobile-chrome(emulace Pixelu 5) amobile-safari(WebKit, emulace iPhonu 15), aby stejné kritické scénáře zachytily desktopové i mobilní regrese. - Na Node 24 nepoužívej Playwright
1.59.1: v GitHub Actions se tento release uměl zaseknout přinpx playwright install --with-deps chromiumpo stažení Chrome for Testing. Drž minimálně1.60.0+; repozitář je aktualizovaný na^1.62.1. - CI po samostatném
npm run buildspouští E2E přímo přesnpx playwright test, aby se kvůlipretest:e2eneprováděl druhý identický build. pretest:e2epro Playwright build musí používat stejný build-time origin jako následnýnext start: nastavujeNEXT_PUBLIC_APP_URL=${PLAYWRIGHT_BASE_URL:-http://127.0.0.1:3100}a zároveň dummy analytics env pro Matomo a Meta Pixel (NEXT_PUBLIC_MATOMO_ENABLED=true,NEXT_PUBLIC_MATOMO_URL=https://matomo.example.test/,NEXT_PUBLIC_MATOMO_SITE_ID=1,NEXT_PUBLIC_META_PIXEL_ENABLED=true,NEXT_PUBLIC_META_PIXEL_ID=123456789). Jinak admin login/logout redirecty a proxy ochrana v produkčním buildu zůstanou inlinované na jiný host a browser testy po přihlášení skončí cross-originERR_CONNECTION_REFUSED; Matomo smoke testy zase neuvidí žádné_paqvolání, protože tracking byl vypnutý už při buildu.- Stejné dummy analytics env musí mít i každý samostatný CI
next buildkrok.NEXT_PUBLIC_*hodnoty se inlinují už při buildu, takže pouhé runtime env vplaywright.config.tsnestačí, pokud test běží nad předem sestaveným.next. - Reschedule scenar
client can reschedule a booking through a public tokenma zamerne sirsi test timeout nez ostatni scenare, protoze overuje plny self-service submit a success render nad produkcnimnext startserverem. - Pri finalnim cekani na success heading je timeout zamerne navyseny na
30_000 ms; pri nezdaru test navic vypise posledni viditelnou chybu formulare, aby CI log hned ukazal, jestli slo o konflikt slotu, validaci nebo obecny save error. - Admin smoke scenare
owner can open the core backoffice sectionsasalon role can open the operational workspace but not owner-only sectionsmají explicitní timeout90_000 ms, protože sekvenční průchod více backoffice rout v CI běžně přesahuje výchozích45_000 ms. - Playwright konfigurace používá lokální produkční
next startserver naPLAYWRIGHT_PORT(výchozí3100) a nastavujeNEXT_PUBLIC_APP_URLna stejný lokální origin pro runtime serveru. - E2E runtime ukládá
SITE_SETTINGS_SNAPSHOT_PATHdo/tmp, aby testovací server nezapisoval do produkční cesty/var/lib/ppstudioa CI nelogovalo chybu oprávnění. - Protože
NEXT_PUBLIC_APP_URLse inlinuje už přinext build, musí stejnou hodnotu dostat i build krok. CI i lokálnípretest:e2eproto buildí sNEXT_PUBLIC_APP_URL=http://127.0.0.1:3100(neboPLAYWRIGHT_BASE_URL) a současně držíNEXT_PUBLIC_SITE_URL=https://ppstudio.cz, aby veřejné canonical/JSON-LD URL zůstaly produkční. - Playwright runtime pro smoke coverage Matoma a Meta Pixelu navíc nastavuje dummy
NEXT_PUBLIC_MATOMO_*aNEXT_PUBLIC_META_PIXEL_*hodnoty, aby server komponenty přinext startskutečně renderovaly analytics trackery i v testovacím prostředí. - E2E fixture helper seeduje unikátní služby, sloty, klienty, tokeny a dočasného owner uživatele přes Prisma; cleanup filtruje podle unikátního
runId, aby testy nesahaly na ručně vytvořená data. - U admin voucher pickeru nevaž Playwright locator na výchozí
aria-pressed="false"stav konkrétní služby. E2E fixture služby používají velmi nízkýsortOrder, takže testovaná služba může být v CI už předvybraná jako první aktivní položka; bezpečný test kliká obecnýbutton[aria-pressed]podle názvu služby a teprve potom ověříaria-pressed="true". - Aktuální E2E smoke coverage ověřuje veřejné vytvoření pending rezervace, self-service storno, self-service přesun a owner potvrzení rezervace v admin detailu.
tests/e2e/booking-flows.spec.tsobsahuje i cleanup regrese pro veřejné rezervace:- seeduje službu s
cleanupMinutes=10, vloží blokující potvrzenou rezervaci se snapshotemcleanupBlockMinutes=15a ověří, že veřejný výběr nenabídne start naserviceEnd, ale nabídne první start až nablockedUntil - zvláštní scénář s jediným hodinovým slotem ověřuje, že poslední klientský start v okně zůstane rezervovatelný i tehdy, když cleanup přeteče za konec slotu
- oba scénáře zároveň hlídají, že souhrn zůstává klientský (konec služby bez interního textu o úklidu)
- seeduje službu s
- U self-service přesunu (
tests/e2e/booking-flows.spec.ts) je kolizní krok záměrně deterministický: test nejdřív vybere konkrétní UI slot, seedne k němu runtime konflikt v DB a až potom submituje změnu, aby vždy ověřil chybovou hlášku před úspěšným přesunem na jiný čas. - Kolizní a úspěšný náhradní čas v E2E fixture drž jako samostatné navazující published sloty se stejným veřejným popisem. Runtime konflikt pak blokuje jen zvolený hodinový segment a následný úspěšný submit není závislý na kapacitní validaci uvnitř jednoho dlouhého slotu.
- Po konfliktní odpovědi u self-service přesunu už je potvrzovací tlačítko povolené z předchozí volby; test proto při výběru náhradního slotu musí čekat na
aria-pressed=truea shodu hiddenslotIdinewStartAt, jinak může rychlý CI běh odeslat starý kolidující termín nebo nový čas se starým slotem. - Náhradní slot u self-service přesunu nevybírej přes
nth(...)ani přes "další dostupné tlačítko". Fixture musí exportovat explicitní accessibility labely nebo ISO start časy pro konfliktní i úspěšný termín, aby test nebyl citlivý na změny 30min generovaných startů ani na pořadí renderu. BookingManagementPanelpro veřejný self-service přesun používá u cílových sekcíscroll-mt-*a vselectDate/selectSlotokamžitýscrollBookingManagementTargetIntoView(...), který měří skutečnou.site-header--bookingvýšku a scrolluje přeswindow.scrollTo({ behavior: "auto" }). Nepřepínej tyto programové skoky zpět na smooth scroll bez ověření Playwrightem, protože rychlé klikání na vzdálenější sloty může proběhnout během animace a prvek pak není stabilní ve viewportu.- Pokud explicitní reschedule slot není mezi prvními navrženými dny, E2E helper
selectSlotById(...)nejdřív z accessibility labelu vytáhne datum a klikne na kalendářové tlačítkoVybrat den ...; až potom hledá přesný čas. Díky tomu test nepadá na obecnémnth(...)fallbacku jen proto, že vzdálenější slot ještě není vyrenderovaný v sekci vybraného dne. - E-mailový worker předává
EmailLog.processingTokenaž do doručení i zápisu výsledku; žádný nový calldeliverEmailLognesmí token obcházet. Mimo worker (např. ruční „vytvořit a odeslat“) se job nejdřív atomicky claimuje přesclaimEmailLogForImmediateDelivery(...). Resend REST dostává stabilníIdempotency-Keyemail-log/<EmailLog.id>; pro obecné SMTP je stabilníMessage-IDpouze pomůcka pro deduplikaci, ne garance exactly-once. - Při paralelním CI běhu může i fixture „úspěšný“ slot po runtime kolizi zůstat obsazený jinou aktivní rezervací z jiného workeru. Reschedule E2E scénář proto po prvním neúspěšném submitu fallbackově zkouší další dostupné sloty, dokud neuvidí success heading nebo diagnostickou chybu formuláře.
- Fallback helper ve stejném scénáři nesmí blokovat na
locator('input[name=\"slotId\"]').inputValue(): po kliknutí na kandidátní slot nejdřív ověř rychlý success stav a při chybějícím/odpojeném hidden inputu pokračuj dalším kandidátem, jinak CI skončí na globálním timeoutu testu bez doménové regrese. - Veřejný booking rate-limit sdílí tabulku
BookingSubmissionLogs admin loginem a veřejným ověřením voucheru, ale při počítání pokusů musí ignorovat prefixyADMIN_LOGIN_aPUBLIC_VOUCHER_VERIFY_. E2E fixture před tvorbou nových dat čistí krátké auditní okno, aby opakované lokální běhy nespouštěly rate-limit napříč nesouvisejícími scénáři. - U veřejného booking flow v E2E testech preferuj
getByRole("textbox", { name: "E-mail" })/getByRole("textbox", { name: "Telefon" })před regexy, které předpokládají, že nápověda je součástí accessible name. Pokud test zakládá vlastní službu bez preselectu slugem, nejdřív otevři její fixture kategorii; stránka může být zároveň seedovaná jinými E2E kategoriemi. - DB integrační testy
booking-public-voucher.integration.test.tsjsou seedované per test case (withSeed(...)) místo sdíleného global seedu, aby paralelní běh netvořil write konflikty přes společnéservice/slot/voucherzázemí. - Při úpravách Playwright locatorů preferuj stabilní business orientační body (sekce, heading, finální CTA) před přesnými dynamickými timestamp labely nebo historickými texty formulářů; admin detail rezervace aktuálně používá pole
Volitelný důvoda submit text odpovídá zvolené akci, např.Potvrdit rezervaci. - Booking detail v adminu používá statickou hlavičku (bez sticky/plovoucího chování), aby nepřekrývala
Další krokani status chooser. - CI workflow
.github/workflows/ci.ymlběží na push domain/master, na pull requesty a lze jej ručně spustit přes GitHub Actions. Hlavní kontroly jsou samostatné jobylint,typecheck,test,build,e2e,e2e chromium shard 2,e2e mobile (mobile-chrome),e2e mobile (mobile-safari),e2e mobile shard 2 (mobile-chrome)ae2e mobile shard 2 (mobile-safari), takže GitHub UI i branch protection vidí každý check zvlášť. Jobtestpo PostgreSQL service containeru,prisma migrate deploya generování klienta spustínpm run test:ci: coverage unit vrstvy a následně sériové DB integrace; coverage report je jeho artefakt. Šest E2E jobů každý připraví vlastní PostgreSQL a build a spustí jeden shard Playwrightu: Chromium 1/2, mobile Chrome 1/2 a mobile Safari 1/2; mobilní Chrome instaluje Chromium a mobilní Safari WebKit. Joblintnavíc bez PostgreSQL spouštínpm run test:release-script. - Po rozdělení do samostatných jobů už mezi nimi nesdílíme pracovní adresář ani
.next.typecheckproto ponpm ciexplicitně pouštínpm run db:generate,buildmá vlastní PostgreSQL service +prisma migrate deploykvůli route/page datům čteným při buildu ae2esi dělá vlastní build ještě před Playwright startem. - CI používá testovací env hodnoty přímo ve workflow a e-maily drží v
EMAIL_DELIVERY_MODE=log, aby browser a DB testy nevytvářely reálné SMTP side effects. - Repo-level enforcement zůstává mimo git: po přidání nebo přejmenování workflow ručně slad branch protection / rulesets a required status checks v GitHub nastavení repozitáře, jinak budou nové kontroly jen informativní. Required checks hlavního CI mají mířit na přesné GitHub job names
lint,typecheck,test,build,e2e,e2e chromium shard 2,e2e mobile (mobile-chrome),e2e mobile (mobile-safari),e2e mobile shard 2 (mobile-chrome)ae2e mobile shard 2 (mobile-safari);coveragesem nepatří, protože samostatný status check nevytváří.
- Sekce
volne-terminyje znovu aktivní jako týdenní planner nad 30min gridem. - Serverový read/persistence model je v
src/features/admin/lib/admin-slots.ts. - Server action adaptéry planneru jsou v
src/features/admin/actions/slot-planner-actions.ts. - Jediným produkčním UI je FullCalendar: serverový wrapper
AdminWeeklyPlannerPagepoužívá klientský kalendářAdminWeeklyPlannerClientpro OWNER i SALON. Soubory si kvůli kontinuitě historie zatím ponechávají původní název*-lab-*; nejde o samostatný experimentální workflow ani veřejnou route. src/components/layout/admin-shell.tsxje klientský kvůli mobilnímu draweru sidebaru; desktop shell zůstává záměrně úzký, aby většina šířky patřila planner gridu.- Pravý panel planneru je akční inspektor dne; na menších breakpointech se otevírá jako
MobileInspectorSheet. src/config/navigation.tsdrží centrální definici admin sekcí, slugů a navigace pro obě role.src/features/admin/components/admin-sidebar-nav.tsxje klientská navigace s aktivním stavem podle pathname.- Sdílené admin sekce pro
OWNERiSALONzahrnují takévouchery; pokud se přidává detail, tvorba nebo čerpání voucheru, zachovej paralelní URL tvar/admin/vouchery/*a/admin/provoz/vouchery/*, pokud role nemá být záměrně omezená. src/features/admin/components/admin-overview-page.tsxje po redesignu jen tenký server wrapper; skutečný overview workspace skládásrc/features/admin/components/admin-dashboard-page.tsx.src/features/admin/lib/admin-dashboard.tsdrží serverový read model pro operativní dashboard dne:- hero
Dnes - alerty
- timeline rezervací + volných oken
- spodní KPI
- pravý sidebar stats / pending / upcoming slots / quick actions
- hero
admin-section-page.tsxdál obsluhuje generické nebo sekundární sekce; overview už na něj nenavazuje.- Druhé kolo visual polish overview je čistě prezentační: neměň read model ani sekce, pokud ladíš jen proporce, spacing, typografii nebo card rhythm.
- Pro mobilní breakpointy overview dashboardu preferuj single-column stack před zmenšováním desktop kompozice: CTA pod sebe, alert CTA na vlastní řádek a quick actions až od
smdo dvou sloupců. src/features/admin/components/admin-booking-detail-page.tsxskládá detail rezervace jako decision-first layout:- statická kompaktní hlavička s
klientka -> služba -> termín -> stav/zdroj -> rychlé akce - horní akční panel jako hlavní rozhodovací centrum
- levý sloupec
akce -> poznámky -> historie - pravý sloupec
souhrn -> technická metadata - na mobilu stejné pořadí bez bočního sloupce
- statická kompaktní hlavička s
- Při dalších úpravách detailu rezervace drž vizuální prioritu
termín + klientka + stav -> další akce -> souhrn -> poznámky -> historie; nenechávej zpět narůst dlouhé vysvětlující texty ani duplicity mezi headerem a souhrnem. src/features/admin/components/admin-booking-status-form.tsxzůstává malou klientskou vrstvou jen pro action chooser a submit server action; reschedule zůstává oddělený vRescheduleBookingButtona při dalších úpravách nenechávej do chooseru vracet slotový nebo drawer flow.src/features/admin/components/admin-booking-note-form.tsxje oddělená klientská vrstva jen pro samostatnou editaci interní poznámky rezervace; drž ji bez dalších provozních rozhodnutí nebo statusové logiky.Client.emailje nově volitelný kvůli ručním rezervacím z Instagramu, telefonu a offline domluvy. Veřejný booking si vlastní povinný e-mail validuje zvlášť, ale admin tok i navazující CRM read modely musí bezpečně počítat snulla používat fallbackBez e-mailunebo podmíněnémailto:odkazy.src/features/admin/lib/admin-users.tsje serverový read model pro owner-only správu přístupů; skládá dohromady DB účty a systémové přístupy z bootstrap env vrstvy.- Pro klientsky bezpečné badge a texty kolem rolí/stavů používej
src/features/admin/lib/admin-user-presentation.ts; nesahej pro runtime helpery do serverového read modelu, jinak se do klienta natáhne Prisma nebo session vrstva. AdminUsermá volitelné poleinvitedAt, které drží čitelný stavPozvánka čekáv owner UI.- Databázové přístupy používají heslo uložené v
AdminUser.passwordHash; hash/verify helper je vsrc/lib/auth/password.ts. - Pozvánky používají model
AdminUserInviteToken(hash tokenu, expirace, použití, revokace) a route/admin/pozvanka/[token]. - Server actions pro owner správu přístupů jsou v
src/features/admin/actions/admin-user-actions.ts:saveAdminUserAccessActionpro založení pozvánky nebo úpravu jména/e-mailu; při nové pozvánce zároveň odesílá invite e-mailchangeAdminUserRoleActionpro jednoduché přepnutí meziOWNERaSALONsetAdminUserActiveActionpro deaktivaci / opětovnou aktivaciresendAdminUserInviteActionzůstává server action vrstva pro sdílenou logiku, ale UI resend v řádku uživatele je záměrně napojené přes API routesrc/app/api/admin/users/resend-invite/route.ts, aby bylo spolehlivé i v klientském list row workflow
- Invite e-mail je záměrně posílaný přímo ze server action přes
sendEmail, aby owner viděl výsledek hned po kliknutí bez čekání na background worker. - Dokončení pozvánky řeší
activateAdminInviteActiona klientská komponentaAdminInviteActivationForm; po úspěchu login stránka zobrazuje informační stavinvite=activated. - Aktivace pozvánky je bezpečnostně kritická transakce:
consumeAdminInviteToken(...)zamkne řádek tokenu iAdminUser, vyžadujeusedAt IS NULL,revokedAt IS NULL, neprošlou expiraci auser.isActive = true, a teprve pak atomicky označí token jako použitý a uloží heslo. Nesmí nastavovatisActive. Deaktivace přesdeactivateAdminUserAndRevokeInviteTokens(...)v tomtéž commitu nastaví účet na neaktivní a revokuje všechny nepoužité tokeny. - Regresi ověřuj databázovým testem
src/features/admin/actions/admin-invite-activation.integration.test.ts: scénář deaktivace se starou pozvánkou musí vrátit chybu a ponechat účet bez hesla i neaktivní; dvě souběžné aktivace stejného tokenu smějí mít právě jeden úspěch. - UI sekce
Přístupyje rozdělené do menších komponentAdminUsersWorkspace,UsersList,UserRow,InviteUserDialog,RoleCards,RoleBadgeaAccountStatusBadge. - Systémové účty se v UI vědomě nepopsují jako bootstrap nebo env účty; tenhle slovník zůstává jen v technické dokumentaci a implementaci auth vrstvy.
- Sekce
Rezervacemá vlastní workflow vsrc/features/admin/components/admin-bookings-page.tsxa už neběží přes generický placeholder renderer. src/features/admin/lib/admin-data.tspro rezervace nově vrací URL-driven read model s filtry, klikacími statistikami, seskupenými bloky seznamu a explicitními kontaktními odkazy místo obecnéhotitle/meta/description.- Validace search parametrů pro pracovní přehled rezervací je v
src/features/admin/lib/admin-booking-list-validation.ts; drží hodnoty prostatus,sourcea klikacístat.sourceje kanál rezervace (WEB,PHONE,INSTAGRAM,IN_PERSON,OTHER), zatímco marketingový původ z UTM/referreru patří doacquisition*polí. src/features/admin/components/admin-bookings-toolbar.tsxpoužívánext/formnad stejnou route a má zůstat nízký a kompaktní i na desktopu.src/features/admin/components/admin-bookings-workspace.tsxje klientská vrstva pro click-to-open řádky, keyboard navigaci, selection shell a lehký toast feedback; booking business logika zůstává na server action vrstvě.src/features/admin/components/admin-bookings-quick-actions.tsxje záměrně velmi malá klientská vrstva pro inline akce podle stavu rezervace; složitější workflow dál patří do detailu rezervace.- Na mobilu drž
admin-bookings-page.tsxjako samostatný compact card pattern s pořadímčas -> klientka -> služba -> stav, ne jako smrštěnou desktop tabulku. - Horní statistiky sekce
Rezervacejsou záměrně kompaktní segmented filter v jedné řadě; nepřidávej do nich další CTA ani sekundární texty typuFiltrovat. - Seskupení pracovního seznamu drž jen čtyři bloky
Dnes,Zítra,Později,Dříve;Dnesmá mít nejsilnější vizuální prioritu. - Sticky header rezervačního seznamu je součást provozního UX; při změnách shell layoutu ověř, že zůstane čitelný i při delším scrollu.
- Sekce
Službymá vlastní workflow vsrc/features/admin/components/admin-services-page.tsxa už neběží přes generický placeholder renderer. src/features/admin/lib/admin-services.tsdrží serverový read model pro seznam, provozní warningy, detail služby a předvyplněný create flow.src/features/admin/actions/service-actions.tsnově obsluhuje create, update, duplikaci, quick toggles a reorder; validace zůstává vsrc/features/admin/lib/admin-service-validation.ts.- Editace služby při skutečné změně
priceFromCzkzapisuje audit doServicePriceChangeLog; aktér se mapuje z admin session e-mailu na reálnéAdminUser.id, stejně jako u jiných provozních mutací. Service.cleanupMinutesje interní provozní metadata služby s validací nezáporných celých minut; při vytvoření/přesunu rezervace se snapshotuje doBooking.cleanupMinutesaBooking.cleanupBlockMinutes(zaokrouhlení nahoru na 15 minut).- Booking dostupnost a kolize používají interní interval
scheduledStartsAt -> blockedUntil, ale klientský termín i veřejné texty zůstávajíscheduledStartsAt -> scheduledEndsAtbez zmínky o úklidu. - Při výběru termínu musí publikované okno pokrýt jen samotnou službu; cleanup blokace může přetéct za konec slotu. Backend kolize ale dál musí používat
blockedUntil, aby se navazující slot nebo dodatečně publikované okno nenabídly dřív, než interní blokace opravdu skončí. - Totéž pravidlo drž i planner read model v
src/features/admin/lib/admin-slots/queries.ts:availableIntervalsse musí odečítat proti všem booking blokacím překrývajícím slot, ne jen proti bookingům se stejnýmslotId, jinak inspektor dne ukáže falešnéVolné oknopo cleanup overflowu do sousedního slotu. - Totéž pravidlo drž i admin dashboard read model v
src/features/admin/lib/admin-dashboard.ts: sekceNejbližší volné termínynesmí zobrazit začátek slotu jen proto, žeslot.bookings.length < capacity; musí odečíst všechny blokacescheduledStartsAt -> blockedUntilpřekrývající publikované sloty a navazující volné intervaly sloučit do stejných souvislých oken, jaká vidí planner. - Po zrušení rezervace přes klientský token i přes admin změnu stavu systém nově spouští post-cancel kompaktaci slotů: pokud se kolem zrušeného bookingu dotýkají sousední běžné publikované editable fragmenty a žádný z nich už nemá aktivní rezervaci, helper
src/features/booking/lib/booking-slot-compaction.tsje sloučí zpět do jednoho souvislého slotu a případné historickéCANCELLEDbooking vazby přesune na anchor slot. - FullCalendar planner zobrazuje úklid a rezervace jako samostatné události nad 30min mřížkou; read model proto dál posílá
cleanupBlocksv minutách od začátku planner dne. - Detail služby obsahuje sekci
Homepage, která ukládáisFeaturedOnHomepageahomepageSortOrder; tato pole neovlivňují booking flow ani ceník, jen výběr doporučených služeb na/. src/features/admin/lib/admin-services.tsteď do detailového read modelu přibírá i posledních 10priceChangeLogsvčetně aktéra, aby drawer mohl audit ceny zobrazit bez další klientské fetch vrstvy.- Ve formuláři detailu služby (
admin-service-form.tsx) je textová vrstva sjednocená podVeřejná prezentace;publicIntroje jediný zdroj krátkého textu pro web i rezervační flow, aby se stejný copy neudržoval ve dvou polích. - Aktuální seznam služeb je záměrně group-first a compact-first:
admin-services-list.tsxskládá služby do kategoriíservice-category-group.tsxdrží rozbalovací skupinu jedné kategorieservice-compact-row.tsxrenderuje hustý pracovní řádek službyservice-actions-menu.tsxcentralizuje row actions do menu⋯service-status-badges.tsxdrží zjednodušené badgeAktivní/Neaktivní,Veřejná/Interní, volitelněSkrytá
- KPI strip sekce
Službyse musí počítat jen z aktuálně filtrovaného běžného katalogu, ne z celé databáze. Aktuální provozní definice jeVeřejné služby= efektivně viditelné položky,Kategorie= počet kategorií v aktuálním pohledu,Interní / skryté= položky neaktivní nebo neveřejné v aktuálním pohledu,Vyžaduje kontrolu= položky s reálným warningem. - Toolbar seznamu služeb má zůstat co nejkratší: bez duplicitního mezititulku, bez druhého CTA a se scope běžného katalogu komunikovaným jen přes malé pills. Legenda stavů patří do drobného rozbalovacího prvku
Legenda stavů, ne do samostatného vysokého bloku nad filtry. - Při dalších úpravách sekce
Službydrž prioritu na hustotě seznamu: základní řádek nemá nést dlouhé vysvětlující texty ani plný seznam akcí. - Sekundární provozní kontext služby patří do rozbalené části řádku nebo do pravého detail draweru, ne do výchozího stavu seznamu.
- Sekce
Kategorie služebmá vlastní workflow vsrc/features/admin/components/admin-service-categories-page.tsxa stejně jakoSlužbyobchází generický placeholder renderer. src/features/admin/lib/admin-service-categories.tsdrží serverový read model pro seznam, warningy, detail kategorie a počty navázaných služeb podle stavu.- Nové UI komponenty pro workflow kategorií jsou v
src/components/admin/categories/:CategoryManagementWorkspace.tsxCategoryStats.tsxCategoryFilters.tsxCategoryList.tsxCategoryRow.tsxCategoryDetailPanel.tsxCategoryDetailDrawer.tsxtypes.ts
- Aktuální vizuální verze sekce
Kategorie služebje záměrně blíž finálnímu provoznímu mockupu:CategoryStats.tsxrenderuje kompaktní souhrnnou lištuCategoryRow.tsxdrží hustší řádkový layout s akcemi vpravoCategoryDetailPanel.tsxje klasický formulářový detail se sticky footrem, ne vysoký card stackCategoryDetailPanel.tsxuž neobsahuje samostatné poleVeřejný název; kategorie se napříč adminem a veřejným katalogem opírá jen oname
src/features/admin/actions/service-category-actions.tsnově obsluhuje create, update, optimistic quick toggles, inline reorder i bezpečné mazání prázdné kategorie; validace zůstává vsrc/features/admin/lib/admin-service-category-validation.ts.src/features/admin/components/admin-booking-detail-page.tsxa route dvojice/admin/rezervace/[bookingId]+/admin/provoz/rezervace/[bookingId]drží první produkční workflow pro práci s rezervací.- Sekce
Klientimá vlastní workflow vsrc/features/admin/components/admin-clients-page.tsxa už neběží přes generický placeholder renderer. src/features/admin/lib/admin-clients.tsdrží serverový read model pro seznam klientek, filtry, detail klientky a napojení na historii rezervací.- Přehled klientů je záměrně CRM-kompaktní: horní statistiky jsou nízký 4položkový strip (
celkem,nové za 30 dní,bez kontaktu,s poznámkou), quick filtry se ukládají doquicksearch parametru a kombinují se se stávajícím hledáním, stavem i řazením. - Desktopový seznam klientů drž jako tabulkový pracovní seznam, ne jako card dump; dlouhé e-maily a technické názvy musí zůstat v
truncatekontejnerech. Chybějící kontakt rozlišuj explicitně nabez e-mailu,bez telefonuabez kontaktu. - Testovací klientské profily se v seznamu pouze označují badge
testpodle bezpečných signálů v read modelu (example.com,Voucher Klientka,Kolize,booking-voucher,client-collision); nepřidávej mazání ani destruktivní bulk akce. src/features/admin/actions/client-actions.tsje tenký server action adaptér pro editaci interní poznámky klientky; validace zůstává vsrc/features/admin/lib/admin-client-validation.ts.- Sekce
Médiamá vlastní workflow vsrc/features/admin/components/admin-media-page.tsxa je dostupná v owner i salon oblasti na/admin/mediaa/admin/provoz/media. - Server action adaptéry pro média jsou v
src/features/admin/actions/media-actions.ts; validace vstupu je vsrc/features/admin/lib/admin-media-validation.ts. - Sekci
Médiadrž jako kompaktní pracovní plochu: krátký header, menší statistické boxy, upload panel s dropzónou, tabs s počty a hustší grid 2-3 karet podle šířky viewportu. - Admin karty médií mají kromě typu a publish stavu i text
Použití, aby obsluha rovnou viděla, zda asset patří doO mně,Studia,Kontaktunebo budoucích hero/banner bloků. - Quick publish/unpublish v knihovně má používat samostatnou
updateMediaPublicationAction; po mutaci zachovej aktivní filtr, aby se obsluha nevracela zbytečně naVše. - Sekce
Nastavenímá vlastní workflow vsrc/features/admin/components/admin-settings-page.tsxa už neběží přes generický placeholder renderer. - Formuláře pro
Salon,RezervaceaE-maily a notifikacejsou oddělené do samostatných client komponent a server action adaptérů vsrc/features/admin/actions/settings-actions.ts. - Sekce
Nastavenínově obsahuje i owner-only kalendářový workflow:- server action
updateCalendarFeedAction - client komponentu
AdminCalendarSettingsForm - read model
getOwnerCalendarFeedAdminState() - veřejný feed
/api/calendar/owner.ics
- server action
- Sdílený skeleton formulářů pro
Nastaveníje vsrc/features/admin/components/admin-settings-form-ui.tsx; drží společné styly polí, zprávy po uložení a patičku se submit buttonem, aby se neopakoval stejný markup ve třech sekcích. - Stránka
Nastavenímá nahoře krátký orientační blok, který v jedné větě vysvětlí, co patří doSalon,RezervaceaE-maily. - Tón admin copy je sjednocený napříč hlavními sekcemi tak, aby zůstal klidný, krátký a srozumitelný pro běžnou obsluhu.
- Pro budoucí upload workflow nepřidávej zvláštní implementaci pro owner a salon admin; sdílená feature vrstva je v
src/features/media/lib/media-library.ts. - Produkční slot routy jsou explicitní a nepoužívají generický
[section]detail:/admin/volne-terminy/admin/volne-terminy/[slotId]/admin/volne-terminy/[slotId]/upravit- salon varianta pod
/admin/provoz/volne-terminy/*
src/features/admin/actions/booking-actions.tsje tenký server action adaptér pro změnu stavu i samostatnou editaci interní poznámky rezervace; aktéra mapuje z admin session na reálnéAdminUser.idpodle e-mailu a při nenalezení používánull, aby zápis historie nenarazil na FK.src/features/admin/components/admin-booking-status-form.tsxpoužívá pro volbu změny stavu klikací akční karty (ne select); vybraná akce se okamžitě zvýrazní barvou podle typu změny a do server action se posílá přes hiddentargetStatus.src/features/admin/lib/admin-booking.tsdrží detailový read model, mapování povolených přechodů, samostatnou poznámkovou mutaci a zápis doBookingStatusHistoryvčetně jednoduchého mapování zdroje změny pro timeline.- Pohled
E-mailyv/admin/logyzáměrně nezobrazuje placeholder tracking poleOtevřenoaKliknutojako plné sloupce; tracking badge vychází z reálných webhook eventů uložených naEmailLog(trackingDeliveredAt,trackingOpenedAt,trackingClickedAt,trackingBouncedAt,trackingFailedAt,trackingSuppressedAt) a bez eventů drží fallbackTracking připraven. - Resend webhook endpoint je
POST /api/webhooks/resend; streamovaně omezuje raw body na 256 KiB, nad limit vrací413a podpis ověřuje přessvix-id,svix-timestamp,svix-signatureaRESEND_WEBHOOK_SECRETnad přesnými raw bytes. Event se páruje přesEmailLog.providerMessageId === data.email_id. - Pokud používáš Resend delivery tracking, preferuj
EMAIL_TRANSPORT=resend; provider při odeslání ukládá Resendemail_iddoproviderMessageId, takže webhook párování je jednoznačné. - Resend delivery taxonomie:
email.bounced,email.failedaemail.suppressedjsou delivery failures (Nedoručeno) a vstupují do aktivního počtu delivery incidentů v chráněné health diagnostice;email.complainedznamená, že příjemce označil e-mail jako spam. Complaint je samostatný reputační warning (Nahlášeno jako spam) v historii aPozornosti, ale není delivery failure. Všechny tyto provider eventy posílají owner Pushover jen při prvním zapsání konkrétního timestampu naEmailLog, aby se předešlo spamování při opakovaných webhook retries. EmailLogzachovává historický transportní i provider delivery stav. Ruční resend zakládá explicitní chain (resendOfId, stabilníresendRootId); webhookemail.deliveredpro resend idempotentně uzavře incident kořenové zprávy jakoDELIVERED_RESEND. OWNER může aktivní root incident ručně uzavřít jakoMANUALs časem, autorem a důvodem, bez založení e-mailu či notifikace. Bounce tedy zůstane v historii jako nedoručený, ale po následně doručeném resendu nebo auditovaném ručním uzavření už nepatří doPozornostani do počtu aktivních e-mailových problémů. SamotnýSENT, další bounce nebo jiný lifecycle e-mail incident neuzavírá.- Meta
Další pokusse v přehledu emailů renderuje jen pro stavypendingaretry, aby úspěšně odeslané záznamy zůstaly provozně čitelné. - Lifecycle klientských booking e-mailů má dva samostatné kroky, nejde o duplicitní odeslání: veřejná
PENDINGžádost vytvoříEmailLog.type = BOOKING_RECEIVEDse šablonoubooking-confirmation-v1(Přijetí rezervace); teprve přechod naCONFIRMEDvytvoříEmailLog.type = BOOKING_CONFIRMEDse šablonoubooking-approved-v1(Potvrzení rezervace). Read model pro starší záznamy stále rozlišuje šablony podletemplateKey. src/features/admin/components/admin-email-log-detail-page.tsxa route/admin/email-logy/[emailLogId]jsou rozdělené do business-first blokůEmailDetailHeader,EmailStatusBadge,EmailQuickActions,EmailSummaryGrid,EmailLinkedEntities,EmailErrorPanelaEmailTechnicalDetails; všechny drží stejný kompaktní density pattern jako hlavní seznam, quick actions běží jako nízká operační lišta a linked entities používají řádkový layout místo vysokých karet.src/features/admin/lib/admin-data.tspro detail emailu dopočítává jediný finální stav podle pravideldelivery failure -> Nedoručeno,sentAt -> Odesláno,PENDING + pokusy -> Retry,FAILED -> Selhalo, jinakČeká, aby se v UI nemohlo potkat současněOdeslánoiRetry.- Technická data detailu zůstávají server-rendered bez nové client state logiky: rozbalení debug bloku i citlivých údajů používá nativní
details/summary. - Payload a raw metadata se před zobrazením maskují podle klíčů typu
token,hash,secret,signature,key,code; plná data zůstávají dostupná jen po rozbaleníZobrazit citlivá data. - Po úspěšné akci detail vrací server-rendered flash banner přes query parametr, aby obsluha viděla okamžitou zpětnou vazbu bez client state.
src/features/admin/lib/admin-data.tsje čistá serverová read vrstva pro admin dashboardy a sekce.- Admin sekce
Rezervaceuž neřeší jen list/detail/stavové akce; obsahuje i plnohodnotný drawerCreateManualBookingDrawers provozním formulářem pro ruční vytvoření rezervace. - Drawer je rozdělený do menších komponent (
BookingClientSelector,BookingServiceSelector,BookingTimeSelector,BookingSourceField,BookingNotificationOptions,BookingInternalNoteField), aby šel stejný workflow později otevřít i z detailu klientky nebo z kalendáře. src/features/admin/actions/booking-actions.tsnově obsahuje i server actioncreateManualBookingAction; pořád je to jen adaptér, skutečný create engine zůstává ve feature vrstvě booking domény.- Operativní overview dashboard má vlastní read model mimo
admin-data.ts, aby se layout dneška a sekundární sekce nevyvíjely ve stejné obecné struktuře. - Dashboard read model
src/features/admin/lib/admin-dashboard.tsvrací jen data, která overview skutečně renderuje: akční alerty s prioritou (primary / secondary / ok), kompaktnítodayPlanItems, provozní KPI,upcomingSlots, týdenní souhrn a rychlé akce. Když přidáš nové pole, ověř, že má skutečného konzumenta v UI. src/features/admin/components/admin-dashboard-page.tsxskládá overview v pořadíProvozní přehled -> alerty -> KPI -> Dnešní plán / Nejbližší volné termíny -> pravý podpůrný sloupeca drží i serverový skeleton fallbackDashboardPageSkeleton.- Dnešní dashboardový plán i rozšířená timeline zobrazují u dnešních rezervací existující poznámky s původem
Klientka/Interně; bez poznámek nevypisuj placeholder, aby cockpit zůstal rychlý. src/features/admin/components/admin-overview-page.tsxpoužíváSuspensenad async serverovým read modelem, takže overview umí zobrazit loading skeleton bez nové klientské state vrstvy.- Lite admin záměrně nepoužívá technický jazyk ani sekce typu nastavení, email logy nebo správa uživatelů.
- Pro
SALONdržíme kratší menu a na úvodní obrazovce zviditelňujeme dnešní rezervace, nejbližší termíny a rychlé akce pro přidání slotu nebo otevření rezervace. salonAdminNavigationse skládá ze stejné centrální definice sdílených sekcí jako owner navigace, aby route guardy, dostupné URL a menu nemohly časem ujet od sebe.- Dynamické admin routy jako
/admin/[section],/admin/provoz/[section]a/admin/email-logy/[emailLogId]mají vlastní layouty se stejnýmAdminShell, aby se neztratil admin vizuál ani ochrana při přímém vstupu na detailní URL. - Sdílené route wrappery pro owner/salon jsou centralizované v
src/features/admin/lib/admin-route-factories.tsx; route soubory vsrc/app/(admin)/admin/**/page.tsxmají být jen tenké entrypointy s předánímarea. - Sdílený layout wrapper
src/features/admin/components/admin-shell-layout.tsxje jediný zdroj truth pro admin shell layout v section/slots/email-log/detail větvích. - Vizuální stabilita adminu je primárně v:
src/components/layout/admin-shell.tsx(šířky sloupců, sticky sidebar, anti-overflow)src/features/admin/components/admin-page-shell.tsx(responzivní nadpisy, stat karty, spacing pro sekce mimo overview)src/features/admin/components/admin-dashboard-page.tsx(přesná hierarchie a hustota overview dashboardu)
- Mobilní drawer navigace v
admin-shell.tsxmá zůstat široká na pohodlné tap targety; při dalších úpravách ji nestlačuj pod aktuálnímin(92vw, 360px)bez konkrétního důvodu. - Při otevřeném mobile draweru v
admin-shell.tsxschovávej horní sticky bar; poloprůhledný overlay jinak nechává prosvítatMenuheader a vizuálně se pere s navigací.
- Route soubory držet tenké, byznys logiku přesouvat do
features,contentalib. - Komponenty pojmenovávat podle odpovědnosti, ne podle umístění na stránce typu
Section1. - Server-side validaci preferovat před klientskými závislostmi.
- Nezakládat univerzální
utilssložky ve feature vrstvách bez jasné potřeby. - U veřejného webu nepřidávat efektní animace bez jasného UX důvodu.
- Většinu právních stránek drž na sdílené
LegalPageskladbě; finální texty a pořadí sekcí patří dosrc/content/public-site.ts, ne do rout nebo nahodilých JSX bloků. - Výjimkou je
/storno-podminky, které používá specializovanouCancellationPolicyPage, protože potřebuje akční hero, praktický kontaktní box a rychlý přehled pravidel ještě před detailními sekcemi. - Pokud upravuješ copy storna, drž ho věcně a konkrétně, ale nestrkej do něj tvrdší sankce, které salon reálně neuplatňuje; kanály pro akci mají vždy zmiňovat telefon, e-mail a podle kontextu i odkaz v potvrzení rezervace / reminderu.
- Pokud právní stránka potřebuje číslovanou osnovu, používej u
LegalSectionpoleeyebrowmísto ručního číslování přímo v layout komponentě. - Booking mutations držet ve feature service vrstvě a server action používat jen jako tenký vstupní adaptér.
- Admin změny stavu rezervace validovat server-side proti povoleným přechodům a nikdy je neřídit jen podle toho, co UI zrovna nabízí v selectu.
- U admin katalogu služeb a kategorií preferuj query-driven vstup do workflow (
mode,mobileDetail) před zaváděním nové routy, pokud cílem není nový samostatný workflow. - Rychlé provozní akce v seznamech řeš server actions; u kategorií je povolený lehký lokální optimistic state přes
useOptimistic, ale bez další state-management knihovny.
- Klíčová rozhodnutí zapisuj jako krátké ADR záznamy.
- Uveď důvod, alternativy a dopad.
- Prisma schema definuje v1 základ pro správu služeb, slotů, klientů a rezervací.
- Prisma 7 CLI konfigurace je v
prisma.config.ts, ne vschema.prisma. - Runtime Prisma klient používá
@prisma/adapter-pg+pg, protože Prisma 7 vyžaduje pro PostgreSQL explicitní driver adapter. AdminUserzůstává oddělený od klientských kontaktů; klientská vrstva je modelovaná přesClient.AvailabilitySlotje navržený jako ručně publikovatelný termín se stavem zveřejnění; PP Studio má jeden obslužný zdroj, protocapacitymusí být vždy1a není to model pro více souběžných klientek.- Pro admin planner je
AvailabilitySlotstále hlavní provozní entita; 30min grid je jen editační vrstva nad souvislými intervaly. AvailabilitySlotmá explicitníserviceRestrictionMode, takže admin rozhraní pozná rozdíl mezi slotem bez omezení a slotem, který čeká na výběr služeb.- Vazba
AvailabilitySlotServiceumožňuje omezit slot jen na vybrané služby bez zabetonování schématu na jednu službu na slot. - Veřejný booking flow rezervuje konkrétní interval uvnitř
AvailabilitySlotpodle vybranéhostartsAta délky služby; planner proto při ukládání půlhodiny stále skládá do souvislých oken. - Ruční admin booking používá stejné create jádro jako veřejný booking; rozdíl je jen ve vstupu, volitelných notifikacích a metadatach rezervace.
- Model
Bookingnyní nese:sourcejako kanál vytvoření rezervace (WEB,PHONE,INSTAGRAM,IN_PERSON,OTHER), kdeINSTAGRAMznamená ruční rezervaci z Instagram zprávy, ne webovou návštěvu s UTMisManualpro rozlišení admin vytvořenímanualOverridepro audit interní výjimky mimo veřejnou dostupnost
- Planner přímo upravuje jen jednoduché publikované sloty bez rezervací, bez poznámek, bez omezení služeb a s kapacitou
1; ostatní zůstávají v UI viditelné jako uzamčené nebo neaktivní. - Pokud slot obsahuje rezervaci jen v části intervalu (např. booking 09:00-09:30 v okně 08:00-14:00), planner ho ve vizualizaci dělí na rezervovanou část a chráněný zbytek, aby celý interval nezmizel z mřížky.
- Import kategorií a služeb je řešený jako JSON upsert přes
scripts/import-services.mjs; identity záznamů držíslug. - Jednorázový backfill strukturované copy služeb v DB je oddělený od importu katalogu a běží přes
scripts/backfill-service-copy.ts. Skript používáDATABASE_URL, defaultně dělá dry-run, ostrý zápis vyžaduje--confirm, hledá jen známé slugy zservice-copy-overrides.tsa zapisuje pouzeseoTitle,idealFor,includes,benefitsagoodToKnow. - Cleanup testovacích booking dat je záměrně oddělený od resetu celé DB:
scripts/clear-booking-data.mjsmaže rezervace, sloty a navázané provozní logy, ale nechává katalog služeb, admin účty, singleton settings i média. Bookingukládá snapshot jména služby, ceny a času, takže historické rezervace zůstanou konzistentní i po úpravě katalogu.Servicenově odděluje obecnou aktivitu (isActive) od veřejné rezervovatelnosti (isPubliclyBookable); public booking flow vyžaduje obě podmínky a aktivní kategorii.Bookingdrží metadata posledního přesunu (rescheduledAt,rescheduleCount) a reminder queue stavu (reminder24hQueuedAt,reminder24hSentAt); historický self-relation chain zůstává ve schématu jen jako legacy pole a nové reschedule flow ho nepoužívá.BookingRescheduleLogje samostatný auditní model pro doménovou akci přesunu termínu; ukládá původní a nový interval, aktéra a volitelný důvod změny.BookingStatusHistorydrží auditní stopu změn stavu včetně aktéra a strukturovaných metadat.ServicePriceChangeLogdrží auditní stopu změn ceníku služeb v adminu; zapisuje jen skutečné změnypriceFromCzk, ne každý save formuláře.- Voucher databázový základ je v migraci
20260427205720_add_vouchers:Vouchereviduje dárkový voucher jakoVALUEneboSERVICE, stav, kupujícího/obdarovanou, hodnotu nebo snapshot služby a auditní vazbu na admin uživatele. VoucherRedemptionje jediný důkaz skutečného uplatnění voucheru; jedna rezervace smí mít nejvýše jeden takový záznam, protože uplatnění voucheru znamená provozní úhradu rezervace. Veřejné zadání voucheru má ukládat jen záměr, ne čerpání.Bookingmá pro MVP voucher intent přímo na sobě přesintendedVoucherId,intendedVoucherCodeSnapshotaintendedVoucherValidatedAt; samostatnýBookingVoucherIntentse zatím nezavádí.- Další business logiku voucherů drž pod
src/features/vouchers; admin UI, public booking napojení a PDF generování nejsou součástí databázové foundation migrace. BookingActionTokenukládá hash tokenu, expiraci a použití/revokaci pro bezpečné self-service storno, self-service změnu termínu a provozní email akce. Raw token je jen dočasně v token-bearingEmailLog.payloadpro PENDING/retryable failure outbox; po SENT nebo terminálním FAILED se známá URL pole redigují.CalendarFeeddrží owner subscription feed jako samostatnou entitu mimoSiteSettings; ukládá scope, aktivaci, rotační salt a audit času změny.- Kalendářový token se neukládá jako raw secret do DB. URL se odvozuje serverově z
CalendarFeed.id,tokenSaltaADMIN_SESSION_SECRET, takže:- admin může odkaz zkopírovat kdykoli
- po rotaci stačí změnit
tokenSalt - po vypnutí se validace zastaví na
isActive = false
- ICS generátor v
src/features/calendar/lib/calendar-ics.tsdrží:- escapování textu podle RFC 5545
- line folding po 75 bajtech
VTIMEZONEblok proEurope/Prague- oddělený mapper
Booking -> VEVENT
- Stejný model
BookingActionTokenobsluhuje i owner/provoz email akceAPPROVEaREJECT; do e-mailu se posílá raw token, v DB zůstává hash a auditní čas použití nebo revokace. Po úspěšném odeslání nebo terminálním selháníEmailLog.payloadzachová jen[REDACTED]v URL polích. - Serverová doménová vrstva pro email akce je v
src/features/booking/lib/booking-email-actions.ts; drží validaci intentu, serializable transakci, změnu stavu, audit a založení klientskéhoEmailLog. src/features/calendar/lib/booking-calendar-attachment.tsgeneruje zákaznickou.icspřílohu z e-mailového payloadu; veřejný booking calendar endpoint ani token typuCALENDARneudržuj.EmailLogje připravený na notifikační workflow a troubleshooting komunikace s klientem.- 24h reminder rezervací je v
src/features/booking/lib/booking-reminders.ts; scheduler vybíráCONFIRMEDbookingy s e-mailem,reminder24hSentAt = nulla dosud nezačatým termínem nejvýšenow + 26h. Kanonický helper rozliší původní 25–26h okno od catch-up fáze, deduplikuje existující current reminder a pozdní admin confirmation, owner approve i ruční potvrzený booking jej volají přímo ve své transakci. - Reminder scheduler neběží jako zvláštní služba;
src/lib/email/worker.tsho spouští uvnitř existujícíhoemail:workerprocesu každých 5 minut a vytváří pouzeEmailLog, nikdy neodesílá SMTP přímo. - Stejný
email:workerkaždých 15 minut spouští bounded cleanup expirovanýchRateLimitReservationzáznamů po dávce nejvýše 500 řádků; chyba cleanupu nesmí zastavit doručování e-mailů a opakování je bezpečné. - Reminder template
booking-reminder-24h-v1je krátký, bez.ics, a používá dvojici bezpečných tokenů proZměnit termínaZrušit rezervaci; copy a layout mají držet lidský tón a rychlou scanovatelnost. - Idempotence reminderu stojí na kombinaci
Booking.reminder24hQueuedAt,Booking.reminder24hSentAta transakčního claimu kandidátky; při přesunu termínu reschedule flow queue marker resetuje, aby se reminder mohl navázat na nový čas bez duplikace starého jobu. - Legacy
Settingzůstává v databázi jako obecné key-value úložiště pro budoucí interní potřeby, ale produkční admin sekceNastavenístojí na explicitním singleton modeluSiteSettings. src/lib/site-settings.tsje centrální read vrstva pro veřejné kontakty, booking pravidla a e-mailový branding; veřejné a e-mailové read cesty už do DB nezapisují a při chybě nebo chybějícím singletonu spadnou na bezpečné defaulty z env/content vrstvy.- Bootstrap
SiteSettingssingletonu zůstává záměrně jen v owner admin workflowNastavenípřes explicitníensureSiteSettings(), takže public metadata, e-mail šablony ani testy nespouštějí write path při obyčejném čtení. SiteSettingsdrží jen skutečně globální provozní hodnoty. Technické env proměnné jako SMTP host/port,NEXT_PUBLIC_APP_URLneboADMIN_SESSION_SECRETse do adminu záměrně nepřenášejí.SiteSettings.voucherPdfLogoMediaIdje nullable legacy FK naMediaAssetzachovaná kvůli kompatibilitě. Aktuální master PDF, admin Settings ani e-mailový renderer tuto vazbu nepoužívají a PDF templates se do Media Manageru nepřidávají.MediaAssetje obecný metadata model pro certifikáty, fotky prostor, reference i další obsahové obrázky; binární obsah zůstává na lokálním filesystemu mimo DB.- Veřejná stránka
/studiopoužívá read modelsrc/features/public/lib/public-studio-photos.ts, který smí vracet jenMediaType.SALON_PHOTOsisPublished = true; komponenty stránky jsou vsrc/features/public/components/studio/studio-page.tsx. - Route
src/app/(public)/studio/page.tsxje aktivní a renderujeStudioPages daty zgetPublicStudioPhotos(); stránka už není schovaná přesnotFound(). - Prolinkování
/studiodo veřejného webu je řízené přesmainNavigationvsrc/config/navigation.ts; položka se automaticky propíše doSiteHeaderi do footer sekceNavigace. - Filesystem layout médií má pro nové uploady tvar
<MEDIA_STORAGE_ROOT>/public/<type>/<year>/<month>/<assetId>-<variant>.<ext>; používáme fixníYYYY/MM, bez jednorázových složek a bez názvů od uživatele vstoredFilename. src/lib/media/local-media-storage.tsje adapter pro lokální filesystem; business vrstva přes něj neřeší konkrétnífsoperace ani fyzické cesty.src/lib/media/media-pipeline.tsdrží lehkou server-side pipeline nadsharp; pro JPEG/PNG/WebP dělá EXIF auto-rotate už na ukládaném originálu,optimizedvariantu (max 1920 px) athumbnailvariantu (cca 400 px) bez zavádění CDN nebo komplexního responsive systému.- Route
/media/[kind]/[[...path]]používá v media path helperu a storage adapteru cílenéturbopackIgnoreanotace u dynamickýchpath.resolve/path.join; cílem je zabránit tomu, aby Next.js 16 Turbopack NFT tracer při buildu omylem zahrnoval celý projekt. - Kanonická veřejná URL pro nové uploady je
/media/public/<type>/YYYY/MM/<filename>; legacy route/media/[kind]/[[...path]]zůstává kvůli starším médiím. - Obě veřejné media routy pouze reexportují
GETzesrc/lib/media/public-media-route.ts; zde zůstává jediná kontrola bezpečné cesty, publikace assetu a bezpečných response hlaviček. - Route pro média umí vrátit originál i varianty
optimized/thumbnailpodle konkrétní storage path uložené vMediaAsset; starší záznamy bez variant fungují dál přes fallback na původnístoragePath. src/lib/media/media-validation.tscentralizuje kontrolu MIME typu, přípony a maximální velikosti souboru.src/lib/media/media-filename.tsgeneruje krátký náhodný asset key a stabilní suffixyoriginal,optimized,thumbnail, takže naming zůstává konzistentní a připravený na další varianty.src/features/booking/lib/booking-public.tsje veřejný write model pro rezervace a drží i ochranu proti souběžnému obsazení slotu.- Veřejný booking write model ukládá k rezervaci i akviziční metadata (
acquisitionSource,acquisitionReferrerHost,acquisitionUtmSource,acquisitionUtmMedium,acquisitionUtmCampaign), pokud jsou dostupná. Admin UI je má prezentovat jakoOdkud přišla, aby se nemíchala s kanálemWeb. - Veřejná rezervace se po submitu vytváří jako
BookingStatus.PENDING; potvrzení (CONFIRMED) je provozní krok z adminu. - Pokud veřejná rezervace zabere jen část delšího slotu s kapacitou
1, booking write model slot v transakci automaticky rozdělí na rezervovaný úsek a zbylé volné fragmenty, aby admin planner zůstal editovatelný po samostatných blocích. src/features/booking/lib/booking-cancellation.tsdrží veřejné storno workflow nad hashovaným action tokenem.- GET načtení
/rezervace/sprava/[token]nesmí vytvářet novýCANCELtoken. Storno URL se vydává až při explicitní server actionstartPublicBookingCancellationAction(...), protože DB záměrně drží jen hash tokenu a existující raw token nelze bezpečně rekonstruovat. - Mazání
BookingPaymentpoužívej přesdeleteBookingPaymentWithAudit(...); kromě smazání platby zapisuje provozní audit doBookingStatusHistorys payment metadaty a admin aktérem. - Admin route handlery s mutací musí používat
isSameOriginAdminRequest(...)nebo ekvivalentníOrigin/Hostkontrolu protiNEXT_PUBLIC_APP_URL; Server Actions zůstávají na vestavěné Next origin ochraně a existujícím session/role ověření. - E-mailové subject/from-name hodnoty validuj přes
src/lib/email/header.ts. CRLF je zakázané, diakritika zůstává povolená a délkové limity řeší konkrétní formulář/schema. src/features/public/lib/public-certificates.tsje veřejný read model certifikátů pro stránku/o-mne; smí vracet jenMediaType.CERTIFICATEaisPublished = true.src/features/public/lib/public-media.tsdrží sdílené read helpery pro publikované obrázky podle typu; homepage čteMediaType.PORTRAIT_HOMEa/o-mnečteMediaType.PORTRAIT_ABOUT; legacyMediaType.PORTRAITuž veřejný web nepoužívá.src/features/public/lib/public-studio-photos.tsje veřejný read model fotek studia; používá ho/studiopro hero + galerii a/kontaktpro hero fotografii./studiopoužívá první dostupnou fotku jako hero a následující dostupné fotky jako galerii (max 6), aby se úvodní vizuál neopakoval.- Galerie
/studiomá responzivní grid pro 1-6 navazujících fotek; při prázdné galerii se sekce obrázků skryje a při chybějícím DB alt textu se používáFotografie prostoru PP Studio. /kontaktpoužívá pouzeMediaType.CONTACT_PHOTO; pokud kontaktní fotka chybí, hero zůstane u placeholderu a nesahá doSALON_PHOTO.src/features/public/lib/public-studio-photos.tspři čteníSALON_PHOTOfiltruje publikované záznamy i podle fyzické existence souboru ve storage (optimizedStoragePath/storagePath), takže veřejný web nezobrazuje broken image pro orphan DB záznamy.- Dev-only fallback pro
/studioje povolený jen přiNODE_ENV=developmentpřespublic/dev/studio/*; produkce vždy čte jen reálná média z DB/storage. - Admin media upload na aktivním filtru používá stejný
MediaTypejako výchozí hodnotu selectu; pro studio fotky tedy nejdřív otevři filtrProstory. - Pro kontaktní hero fotku otevři v admin media filtr
Kontakt; nový upload se tím založí jakoMediaType.CONTACT_PHOTO. MediaAsset.sortOrderje upravitelný v admin media formulářích a veřejný read model jej respektuje přes existující řazenísortOrder ASC, createdAt DESC.- Server actions pro média po uploadu, editaci, publish/unpublish i smazání revalidují
/studioa/kontakt, protože obě stránky čtou veřejnou media knihovnu (SALON_PHOTOpro studio,CONTACT_PHOTOpro kontakt). - Veřejný booking flow vrací doménové chybové kódy a doporučený krok formuláře, takže UI může zobrazit přesnější recovery stav bez duplikace serverové logiky.
- Veřejný booking submit má lehký rate limit podle IP a e-mailu a zapisuje auditní log pokusů, blokací a selhání pro provozní troubleshooting.
- Krok 2 veřejného booking flow filtruje sloty i podle délky služby, aby se krátké sloty neukazovaly až v posledním kroku.
- Krok 1 veřejného booking flow je dvouúrovňový (
kategorie -> služba), ale dál používá stejný katalog aserviceId. - Krok 2 veřejného booking flow nabízí nejdřív
SuggestedSlotss nejbližšími volnými časy a teprve pod nimi kalendářní fallback pro jiný den. - Krok 2 veřejného booking flow používá dvoufázový výběr termínu: rychlá doporučená volba nebo kalendářní výběr dne a následně seznam konkrétních časů pro vybraný den.
- Krok 2 generuje konkrétní starty po 30 minutách uvnitř slotu a zobrazuje jen ty, které se při aktuální kapacitě nekryjí s existujícími aktivními rezervacemi.
- Krok 2 veřejného booking flow drž jako rychlý decision flow: doporučené termíny nahoře, kalendář jako fallback a pod ním větší tlačítka konkrétních časů; detail termínu patří až do souhrnu v pravém panelu.
- Kontaktní krok používá lehkou klientskou inline validaci jen jako UX vrstvu; server-side validace v
create-public-booking.tszůstává autoritativní. - Veřejná route
/rezervacemůže dostat?service=<slug>; klient smí předvybrat jen službu, kterou najde v právě načteném veřejném katalogu. Nesmí vzniknout samostatná trust větev přes service ID nebo jiný bypass katalogu. create-public-booking.tskromě IP/user-agent auditu načítá i cookieppstudio-booking-acqa propsává akviziční kontext doBookingiBookingSubmissionLog.metadata.- Klientský tracker
src/features/booking/components/booking-acquisition-tracker.tsxběží v root layoutu, sbíráutm_*+ externídocument.referrer, normalizuje je a ukládá do cookieppstudio-booking-acq(SameSite=Lax, 30 dní). - Akviziční cookie ukládá pouze relativní
landingPath; scheme-relative hodnoty typu//host/patha backslash varianty se zahazují na/. Referrer hosty se klasifikují jen přes přesnou doménu nebo subdoménu, ne přes volný substring. - Pokud landing URL kombinuje
servicesutm_*nebomtm_*, route ani klientský flow nesmí query přepsat; tracker má zachovat původnílandingPathvčetně marketingových parametrů. - Success stav veřejného booking flow drž jako vlastní confirmation layout, ne jako prodloužený souhrn:
- horní status blok jen pro stav rezervace, s copy
Rezervace přijataaČeká na finální potvrzení - hero text má výslovně říct, že termín je pro klientku předběžně rezervovaný
- hlavní detail rezervace ukazuje samostatně službu, datum a čas; čas zobrazuj s mezerami kolem pomlčky, například
09:30 – 10:30 - density confirmation vrstvy drž kompaktní: menší vertikální mezery, nižší hero i detail card, bez dlouhých samostatných bloků
- detail rezervace preferuj ve skladbě
služba+datum · čas; čas zůstává vizuálně nejvýraznější údaj - referenční kód nezobrazuj, dokud neexistuje samostatné business pole používané v adminu nebo klientské komunikaci
- stručný blok
Co bude následovatmá říct, že potvrzení přijde e-mailem a studio se ozve při potřebě upřesnění - pod další kroky patří krátké uklidnění, že termín je rezervovaný a klientka nemusí dělat nic dalšího
- blok
Potřebujete změnu?, CTAZměnit termína CTAZrušit rezervacina post-submit screen nevracej; tahle obrazovka má flow uzavírat, ne otevírat další rozhodnutí - intro aktivního flow
Vyberte si termín...renderuj jen před formulářem, ne nad confirmation panelem po úspěšném submitu - kontakt na studio až v posledním bloku; na desktopu může být v jedné řádce
email · telefon, na mobilu jako dvě dobře klikatelné akce - booking varianta shellu může mít kompaktnější footer než ostatní veřejné stránky, ale bez změny linků a kontaktů
- horní status blok jen pro stav rezervace, s copy
createPublicBooking()vrací pro confirmation vrstvu ischeduledStartsAt,scheduledEndsAtacancellationUrl, aby web i e-mail nemusely domýšlet další akce z neúplných dat.BookingConfirmationPaneltokenové manage/cancel odkazy z public action payloadu nemění ani negeneruje, ale na post-submit obrazovce je nezobrazuje; bezpečný manage entrypoint zůstává pro e-maily a detail rezervace.- Matomo event
Rezervace / Kontakt zahájense nově posílá až při první reálné interakci s kontaktním polem (focus/input), ne při samotném výběru času; eventyKontakt pole fokus,Kontakt pole vyplnění začátekaKontakt pole chybajsou per-field omezené na první výskyt. - Matomo event
Rezervace / Vytvořenase posílá po success stavu vBookingFlowa chrání hocreatedBookingTrackedRef; nepřidávej další odeslání přímo doBookingConfirmationPanel, aby nevznikaly duplicity při re-renderu. - Matomo event
Rezervace / Služba vybránamusí odcházet i při předvyplnění služby z URL (/rezervace?service=...) bez ručního kliku ve kroku služby; vBookingFlowje na to samostatný jednorázový guardprefilledServiceTrackedRef, aby funnel zachytil vstupy z ceníku/detailu služby bez duplicit. - Self-service změna termínu v
BookingManagementPanelpoužívá stejný princip:Rezervace / Datum vybránoaRezervace / Čas vybránse volají jen v click handlerech, refy brání opakovanému odeslání stejné volby a event name nesmí obsahovat token, klientku ani kontakt. /rezervace/sprava/[token]drž jako produkční decision flow: hero kontext, kompaktní aktuální rezervace, primární nejbližší termíny, sekundární kalendář, sloty pro vybraný den, potvrzení a až nakonec slabé storno. Telefon ani další CTA nevracej vedle potvrzení.- Po kliknutí na den má UI plynule scrollovat na sloty vybraného dne; po kliknutí na slot má nastavit hidden inputs, zvýraznit čas, aktualizovat potvrzení a scrollovat na sekci
Potvrdit nový termínbez klientského redirectu. - Pending confirmation screen po odeslání rezervace záměrně nenabízí
Přidat do kalendáře; kalendářová událost se přikládá až do emailubooking-approved-v1po přechodu rezervace doCONFIRMED. - Transformaci slotů pro krok 2 drž mimo JSX v helperu
src/features/booking/lib/booking-time-slots.ts; UI komponenty mají dostávat už připravenéTimeSlotOption[]a skupiny zgroupSlotsByDayPeriod(). - Kalendářní denní klíče v kroku 2 (
YYYY-MM-DD) generuj locale-agnosticky přesIntl.DateTimeFormat(...).formatToParts(); nepoužívejformat()jako zdroj klíče, protože pořadí/oddělovače se liší mezi prostředími a může rozbít mapování měsíců/dnů. - U kalendářních gridů v kroku 2 drž explicitní
gridTemplateColumns: repeat(7, minmax(0, 1fr))přímo v komponentě jako runtime pojistku; samotná utility třída nemusí v některých prostředích stačit. src/lib/email/*je samostatná infrastrukturní vrstva:- provider řeší SMTP transport
- templates renderují obsah z
EmailLog.templateKey - worker claimuje
EmailLogřádky v background režimu a delivery aktualizujeEmailLog.status,provider,providerMessageId,attemptCount,nextAttemptAtaerrorMessage
- Renderer klientských šablon musí být kompatibilní i se staršími
EmailLog.payload: ubooking-confirmation-v1,booking-approved-v1,booking-reminder-24h-v1abooking-rescheduled-v1jemanageReservationUrlvolitelný fallback; při chybějící hodnotě se nesmí rozbít render ani worker, jen se vynechá případné CTAZměnit termín. - Self-service přesun rezervace (
changedByClient=true) zakládá vedle klientskéhobooking-rescheduled-v1i admin notifikaciadmin-booking-rescheduled-v1nanotificationAdminEmail(pokud je nastavený), aby owner dostal informaci o přesunu mimo admin UI. - Admin šablona
admin-booking-notification-v1má zůstat email-safe a mobilně rozhodovací: inline styly, tabulková plnošířková CTA, Arial/Helvetica pro tlačítka, bez web fontů, bez přehnaného letter-spacing a bez dlouhého vysvětlování procesu. Může obsahovatclientNotejako provozní kontext pro schválení čekající rezervace, ale klientské booking e-maily zákaznickou poznámku dál neposílají. Neměň approve/reject tokenové URL aniadminUrl;Přesunout termínvede na existující detail rezervace v administraci. - Potvrzovací e-mail
booking-confirmation-v1má stejně jako webový post-submit screen držet hierarchii bez CTA: stav -> služba / datum / čas -> místo -> kontakt. booking-reminder-24h-v1nemá samostatné CTAOzvat se studiu; kontakt je jednou ve spodním kontaktním bloku a akceZměnit termín/Zrušit rezervacizůstávají sekundární.- U klientských e-mailů nesmí být storno vizuálně dominantnější než obsah potvrzení nebo připomínky. U admin notifikace je jediná primary akce
Potvrdit rezervaci;Přesunout termínaOtevřít v administracijsou secondary,Zrušit rezervacidanger-light. booking-confirmation-v1,booking-approved-v1,booking-reminder-24h-v1ibooking-rescheduled-v1teď dostávajímanageReservationUrl; token se generuje per e-mail/send, PENDING/retryable FAILED outbox drží raw URL pro delivery/retry a po SENT nebo terminálním FAILED se URL v payloadu redigují. Ruční resend vydává nové tokeny místo kopírování historického payloadu.- Referenční kód rezervace už se v klientském flow záměrně nepoužívá; veřejný web, e-maily i
.icspopis komunikují jen službu, termín a konkrétní akce přes tokenizované odkazy. - Potvrzovací e-mail
booking-approved-v1nově přikládá souborpp-studio-rezervace.ics; attachment se generuje serverově při renderu šablony z payloadubookingId + serviceName + scheduledStartsAt + scheduledEndsAt.
- Stávající bootstrap migrace rozšiřujeme inkrementálně, ne přepisem historie.
- Migrace
20260418184500_schema_v1_booking_corezachovává existující booking data:- vytvoří
Clientz historických rezervací - převádí
BookingRequestnaBooking - backfilluje snapshot služby a času
- převádí single-service sloty na M:N omezení služeb
- vytvoří
- Migrace
20260418193000_booking_model_review_fixesdoplňuje minimální provozní ochrany:- explicitní režim omezení služeb na slotu
- historický reschedule chain pro rezervace (dnes už jen legacy pole)
- původní unique ochranu proti duplicitní rezervaci stejného klienta do stejného slotu (později nahrazenou přesnější variantou na úroveň konkrétního intervalu)
- PostgreSQL exclusion constraint proti překrývajícím se aktivním slotům
- Migrace
20260420153000_booking_exact_duplicate_activenahrazuje širokéUNIQUE(slotId, clientId)za partial unique indexBooking_exact_duplicate_active_key, který blokuje jen přesně duplicitní aktivní interval (slotId + clientId + scheduledStartsAt + scheduledEndsAtpřistatus IN (PENDING, CONFIRMED)). - Migrace
20260710110000_availability_slot_capacity_onenejdřív spočítá sloty scapacity <> 1a při nenulovém výsledku se ukončí bez změny dat. Teprve nad čistými daty nahrazuje původní CHECKcapacity > 0constraintemcapacity = 1. - Migrace
20260423113000_booking_reschedule_logs_v1přidáváBooking.reminder24hQueuedAt,Booking.rescheduleCounta nový auditní modelBookingRescheduleLogpro doménovou akci přesunu termínu. - Migrace
20260424103000_service_price_change_log_v1přidává auditní modelServicePriceChangeLogpro změny cen služeb v adminu. - Nové slot admin workflow nevyžadovalo další migraci; navazuje přímo na už existující schema a constrainty.
- Migrace
20260419103000_service_public_bookabilitypřidáváService.isPubliclyBookablea backfilluje ho podle dosavadníhoisActive, aby se zachovalo chování migrovaných služeb. - Při další iteraci booking workflow preferuj nové migrace nad ruční editací starších SQL souborů.
-
Minimální kontrola při každé změně:
npm run lintnpm run typechecknpm run testnpm run build
-
npm run testnyní skládátest:unita následnýtest:db:integration; guardRUN_DB_INTEGRATION_TESTS=1proto aktivuje DB scénáře jen ve výslovně určené, sériové vrstvě. -
U DB integračních testů, které vytváří
AvailabilitySlotřádky, neseeduj časy z malého fixního okna; při paralelním běhu to může náhodně narážet naAvailabilitySlot_active_time_window_excl. Preferuj UUID/hash odvozený časový rozptyl uvnitř aktuálního booking window. -
Stejné pravidlo platí i pro seed aktivních
Booking, ne jen slotů. Pokud test vybírá konkrétní startsAt ručně, musí předem ověřit i absenci překryvu s jinou aktivní rezervací; jinak se flake projeví jako doménová chybaVybraný termín koliduje s jinou rezervací.místo skutečné regresní změny. -
Pro DB-backed integrační testy booking domény je připravený i:
npm run test:db:booking
-
Pro celou DB integrační vrstvu (včetně admin a voucher scénářů) použij:
npm run test:db:integration
-
npm run test:db:bookingspouští celý booking integrační globsrc/features/booking/lib/*.integration.test.ts, takže vedle reschedule/management scénářů pokrývá i veřejné vytvoření rezervace, ruční admin booking a 24h reminder + e-mail worker flow. -
Pro rychlé unit ověření bezpečné veřejné správy rezervace a reschedule domény můžeš spustit i:
node --import tsx --test src/features/booking/lib/booking-management.test.tsnode --import tsx --test src/features/booking/lib/booking-rescheduling.test.ts
-
src/features/booking/lib/booking-management.tsasrc/features/booking/lib/booking-rescheduling.tsmají záměrně malé dependency factoriescreateBookingManagementApi(...)acreateBookingReschedulingApi(...); slouží jen jako test seam pro mocknutí Prisma/notifikačních závislostí a nemají být druhým produkčním entrypointem s odlišnou business logikou. -
Veřejný klientský reschedule nesmí obcházet pravidla přes admin-style manual override; integrační test pro
/rezervace/sprava/[token]musí ověřovat i odmítnutí termínu mimo online okno, stejného termínu, kolize a terminal stavů bez zápisu historie nebo email side effects. -
npm run devinpm run buildnyní před startem automaticky spouštíprisma generate, takže po změně Prisma schématu nevznikne rozjezd mezi generovaným klientem a runtime admin obrazovkami. -
Pokud měníš e-mail delivery, ověř i
npm run email:worker:once. -
Po změně reminder flow ručně ověř i:
- že
npm run email:worker:oncezapíše reminder kandidátky doEmailLog - že reminder e-mail nevytváří
.icsattachment - že storno nebo přesun rezervace mezi enqueue a sendem skončí
system-skip, ne reálným odesláním - že ruční owner akce
Znovu odeslat e-mailna reminder detailu vytvoří nový log s payload flagemmanualReminderResend=truea tenhle resend se neposuzuje běžným reminder preflight skip pravidlem
- že
-
Před aplikací migrací v prostředí, kde už běžela produkční data, spusť
npm run db:check-migrations; script zkontroluje otevřené failed/incomplete záznamy v_prisma_migrations. -
Před migrací kapacity navíc proveď
SELECT "id", "startsAt", "endsAt", "status", "capacity" FROM "AvailabilitySlot" WHERE "capacity" <> 1;. Nalezené řádky rozhodni a oprav explicitně; migrace je nikdy sama nesnižuje, aby nezakryla možné souběžné rezervace. -
Při změně veřejného webu navíc ručně ověř:
-
Po změně veřejného booking flow ručně ověř i:
/rezervacena mobilu i desktopu- přepnutí kategorie služby a reset vybraného termínu při změně služby
- automatický scroll ze služby na termíny
- sekci
Nejbližší dostupné termínya jedním klikem navázaný přechod na kontakt - kalendářní fallback pro jiný den
- větší grid časů včetně selected/disabled stavů
- inline validaci jména, e-mailu a telefonu
- sticky CTA na mobilu pro stavy
vybrat termín -> doplnit kontakt -> odeslat rezervaci - editaci služby / termínu / kontaktu ze souhrnu bez ztráty vybraných dat
-
Po změně admin katalogu služeb ručně ověř i:
/admin/sluzbyi/admin/provoz/sluzbyna desktopu a mobilu- založení nové služby přes CTA
Nová služba - filtr podle kategorie a kombinaci s fulltextem / stavem / veřejností
- rychlé akce v kartě seznamu (
aktivovat,veřejná/interní,duplikovat,posunout) - mobilní otevření detailu a návrat zpět na seznam
-
Po změně admin katalogu kategorií ručně ověř i:
/admin/kategorie-sluzebi/admin/provoz/kategorie-sluzebna desktopu a mobilu- pravý detail drawer na desktopu i mobilu
- vytvoření nové kategorie přes CTA
+ Nová kategorie - kombinaci fulltextu, stavu, řazení a chip filtrů
- optimistic přepnutí aktivního stavu a posun nahoru/dolů
- warning stavy
prázdná,bez veřejné služby,neaktivní s aktivními službami
-
Po změně sekce
Rezervaceručně ověř i:/admin/rezervacei/admin/provoz/rezervacena desktopu a mobilu- kompaktní řádkový layout bez návratu k vysokým kartám
- sticky header sloupců při scrollu
- inline akce
PotvrditaZrušitbez otevření detailu a zkrácené CTAOtevřít - sloupec
Statusjako samostatný centrovaný grid item CANCELLEDse ve sloupciStatusjen lehce podbarvuje červeně pro rychlejší skenování- ověření, že akce na menších šířkách fungují jako full-width footer a od
lgse vrací do úsporného vlastního sloupce - správné schování neplatné rychlé akce podle aktuálního stavu rezervace
- barevné badge pro
čeká,hotovo,zrušenoa čitelnost kontaktu i zdroje v hustém řádku - detail rezervace s novou kompaktní statickou hlavičkou, jedním souhrnným panelem a kompaktní akční zónou
- samostatné uložení interní poznámky bez změny stavu a propsání do historie
- timeline historie s důvodem, poznámkou a zdrojem změny, pokud je k dispozici
-
Admin sekce
SlužbyiKategorie služebnyní používají sjednocený pravý overlay drawer pattern i na desktopu:Služby: query-driven výběr služby nebomode=createotevře pravý drawer nad seznamemKategorie: list/detail workspace už nepoužívá sticky desktop panel; detail se otevírá přesCategoryDetailDrawerpro všechny breakpointy- přepnutí
Veřejně rezervovatelnáa dopad na/rezervace - změnu délky služby a skrytí slotů, které jsou po změně kratší než služba
- přepínání editace mezi službami z různých kategorií a správné předvyplnění konkrétní vybrané služby
- editaci služby v neaktivní kategorii a očekávané skrytí z veřejného bookingu
-
Po změně admin kategorií služeb ručně ověř i:
/admin/kategorie-sluzebi/admin/provoz/kategorie-sluzebna desktopu a mobilu- založení nové kategorie přes CTA
Nová kategorie - rychlé akce v seznamu (
aktivovat,posunout,zobrazit služby) - CTA
Vytvořit službu v této kategoriia předvyplnění kategorie v sekciSlužby - mobilní otevření detailu a návrat zpět na seznam
- změnu pořadí kategorie a nové řazení na
/sluzby,/cenika v/rezervace - deaktivaci kategorie s navázanými službami a očekávané skrytí z veřejného webu i bookingu
- blokaci mazání neprázdné kategorie a úspěšné smazání prázdné kategorie
-
Po změně admin sekce
Nastaveníručně ověř i:/admin/nastavenina desktopu i mobilu- propsání kontaktů do footeru a stránky
/kontakt - propsání storno limitu do
/faqa/storno-podminky - propsání booking pravidel do
/rezervacea do self-service storna - novou rezervaci i storno s admin notifikačním e-mailem
- mobilní header a CTA na
/,/sluzby,/kontakt - správnost interních odkazů v footeru
- metadata a titulky pro detail služby
-
Po změně Prisma schematu navíc:
npm run db:generatenpm run db:migratenpx prisma validate --schema prisma/schema.prisma
-
Po změně admin voucher detailu ručně ověř owner i salon variantu
/admin/vouchery/[voucherId]a/admin/provoz/vouchery/[voucherId]: provozní editace smí ukládat jen kupujícího, e-mail, platnost a interní poznámku; zrušení musí vyžadovat důvod, odmítnout vouchery s čerpáním a po zrušení schovat opakovanou akci. -
Po změně voucher ověření nebo čerpání ručně ověř, že
CANCELLEDmá prioritu před expirací a zůstatkem, veřejná stránka/vouchery/overenineukáže interní důvod zrušení a admin detail důvod zobrazí. -
Po změně slot admin workflow ručně ověř i:
-
Po změně detailu rezervace ručně ověř i:
/admin/rezervace/[bookingId]a/admin/provoz/rezervace/[bookingId]na desktopu i mobilu- stavy
PENDING,CONFIRMEDi uzavřenou rezervaci bez dostupných dalších akcí - funkčnost rychlých odkazů
tel:amailto: - propsání uložené změny do success stavu formuláře i do bloku
Historie změn
-
Po změně admin sekce
Klientiručně ověř i:/admin/klientia/admin/provoz/klientina desktopu i mobilu- hledání podle jména, e-mailu i telefonu
- přechod do detailu klientky a návrat zpět na seznam
- uložení interní poznámky a propsání změny po refreshi do owner i salon oblasti
- rychlé odkazy
tel:amailto:v detailu klientky - CTA
Vytvořit rezervaciz detailu klientky a otevření/admin/.../rezervace?create=1&clientId=... - předvyplněný blok
Vybraná klientka, fallback hlášku pro neplatnéclientIda warning pro neaktivní klientku - vytvoření ruční rezervace pro předvyplněnou klientku bez obejití kolizní validace
- vytvoření kolizního slotu odmítnuté serverem
- editaci slotu s aktivní rezervací a blokaci neplatného snížení kapacity
- archivaci pouze bez aktivní rezervace
-
Při úpravě lokálního dev serveru nebo
next.config.tsručně ověř i otevření aplikace z vedlejšího zařízení v LAN; pokud browser hlásí blokaci/_next/webpack-hmr, zkontrolujallowedDevOriginsa restartuj dev server. -
Pokud
/admin/sluzbyv devu běží desítky sekund a browser hlásíChunkLoadErrorna/_next/static/chunks/..., nejdřív ověř DB query stopu této stránky: list view nemá bezserviceIdnačítat detail služby, seznam má číst jen nezbytné sloupce a stavové počty musí jít přes jeden agregační dotaz (groupBy) místo vícecount. -
Pokud
/voucheryv devu po prvním pomalém renderu začne v browseru hlásitChunkLoadErrorneboFailed to fetch RSC payload, ověř, že route netahá celý veřejný katalog jen kvůli několika kartám doporučených služeb; landing page má používat limitovaný helpergetVoucherSuggestedServices(3), negetPublicServices(). -
Po změně media vrstvy ručně ověř i:
- vytvoření upload rootu v
MEDIA_STORAGE_ROOT - úspěšné uložení veřejného obrázku a jeho dostupnost přes
/media/public/<kind>/...nebo legacy/media/<kind>/... - odmítnutí nepodporovaného MIME typu a příliš velkého souboru
- smazání assetu v DB i na filesystemu
- vytvoření upload rootu v
-
Po změně certifikátového workflow ručně ověř i:
/admin/mediaa/admin/provoz/mediana desktopu i mobilu- tabs
Vše / Certifikáty / Prostory / Kontakt / Portrét Homepage / Portrét O mněvčetně správných počtů - oddělený portrét pro homepage a
/o-mnebez legacy fallbacku - quick akci
Publikovat/Skrýtpřímo na kartě a návrat do stejného filtru - empty state pro prázdnou knihovnu i prázdný konkrétní filtr
- upload certifikátu a okamžité propsání do
/o-mne - smazání certifikátu a zmizení z adminu i veřejné stránky
- Tajné údaje držet pouze v env.
ADMIN_SESSION_SECRETmusí mít minimálně 32 znaků.- Admin routy nikdy neodemykat jen na základě klientského stavu.
- Cancel/reschedule tokeny generovat jako náhodné tajné hodnoty, do DB ukládat pouze jejich hash.
- Self-service reschedule nikdy nesmí přijímat veřejné
bookingId; jediná veřejná cesta k rezervaci je přes validníRESCHEDULEtoken a server-side znovuověření stavu i časového limitu. - E-mail a telefon normalizovat na vstupu server-side ještě před zápisem do
ClientaBooking. - Telefon klientky ukládej jen přes sdílené helpery v
src/features/booking/lib/client-phone.ts: prázdná hodnota je povolená, českých 9 číslic se normalizuje na+420..., prefix00na+, explicitní mezinárodní předvolba se respektuje a raw vstup s písmeny/HTML nesmí projít ani přes server action, ani přes booking engine. - Při veřejné rezervaci zamknout slot v transakci a znovu ověřit kapacitu až těsně před vytvořením
Booking. - Při serializable konfliktu nebo deadlocku booking flow transakci krátce retryne místo okamžitého pádu na generickou chybu.
- Server-side validace musí znovu ověřit i to, že délka vybrané služby reálně odpovídá délce slotu.
- Pro anti-spam ochranu zapisuj submission logy i pro blokované pokusy, aby šlo dělat provozní audit bez zbytečného přidávání další infrastruktury.
- E-mailové šablony drž jako čisté funkce bez přímé DB závislosti, aby šly jednoduše unit testovat.
- Background worker je provozní proces, ne web request; při nasazení musí běžet odděleně od Next.js serveru.
- Release checklist.
- Migrační kroky (pokud jsou potřeba).
- Hlavní workflow běží na
/admin/volne-terminya/admin/provoz/volne-terminy. - Mobilní FullCalendar nabízí pohled pro den, pracovní dny a víkend bez druhé implementace planneru.
[slotId]a[slotId]/upravitzůstávají kvůli kompatibilitě URL a přesměrují do správného týdne; samostatná routenovyuž neexistuje.- Mřížka používá 28 půlhodinových buněk na den (okno
06:00-20:00). - Výpočet začátku týdne musí vycházet z lokálního kalendářního dne
Europe/Prague(pondělí jako první den), ne zgetUTCDay()nad UTC půlnocí. createdByUserIdpři planner mutacích ber z reálnéhoAdminUser.id; bootstrap session identifikátory (bootstrap-owner,bootstrap-staff) nejsou DB FK a musí fallbacknout nanull.- Seznam
Volná oknav inspektoru dne neklíčuj jen přesstartCell-endCell; po legacy fragmentaci se mohou objevit duplicitní rozsahy. Použij klíč navázaný i nadateKeya pořadí položky, aby planner v browseru negeneroval React warning o duplicitních keys. - Zápis do DB probíhá přes merge/split logiku:
- prázdné nebo zelené buňky se z klienta pošlou jako rozsah buněk
- server z nich spočítá časové hranice v časové zóně
Europe/Prague - den znovu načte ze serveru
- chráněné intervaly (rezervace, omezení služeb, neaktivní sloty, sloty s poznámkou nebo jinou kapacitou) odmítne měnit
- zbylé jednoduché publikované sloty smaže a znovu založí jako minimální sadu souvislých intervalů
mutations.integration.test.tsmá DB integrační scénářgetAdminPlannerWeek(...), který ověřuje, že planner API vrací odděleně klientský čas služby (label), interní blok (blockedLabel,cleanupBlockedUntilLabel) i mapování cleanup overlay buněk (bookedCleanup) pro rezervace se snapshotemcleanupBlockMinutes.
- Ověř owner i salon variantu
/admin/volne-terminya/admin/provoz/volne-terminy. - Ověř přidání a odebrání jednoho půlhodinového bloku kliknutím bez horizontálního scrollu editoru dne.
- Ověř přidání delšího úseku tažením a následné sloučení do jednoho
AvailabilitySlot. - Ověř odebrání části dostupnosti ze zeleného bloku a správné rozdělení na zbylé intervaly.
- Ověř průběžné uložení změny a obnovení uloženého stavu po chybě.
- Ověř, že zásah do rezervace nebo omezeného slotu vrátí srozumitelnou chybu a nic nepřepíše.
- Ověř, že slot s
CANCELLEDrezervací planner neukazuje jako blokaci a nezůstává v mřížce jako šedý nebo uzamčený historický stín.
- Salon-facing time is
Europe/Prague; do not rely on server local timezone or fixed+01:00/+02:00offsets. - Planner slot creation should use
dateKey + half-hour cell indexthroughgetCellRangeBounds(...). - Public booking catalog may return UTC ISO strings, but all client display helpers must render with
timeZone: "Europe/Prague". - Regression tests for DST live in admin slot time helpers, booking formatting, public booking helpers, e-mail templates and ICS utilities.
- Totéž platí i pro admin-side preview lokálně zadaného data/času: drawer
Přidat rezervaciaPřesunout termínnesmí skládat preview přes browser-localnew Date("YYYY-MM-DDTHH:mm"), ale přesresolvePragueLocalDateTime(...), jinak se uložený čas rozjede s UI mimo CZ timezone a kolem DST. manualOverrideje provozní výjimka jen pro explicitní ruční zadání termínu. Pokud admin vybírá konkrétní published slot a ten je stale nebo po změně služby už nevyhovuje, backend musí požadovat nový výběr slotu místo tichého vytvoření draft override slotu.- Při ruční rezervaci pro
selectedClientIdje prázdný e-mail ve formuláři jen „bez změny kontaktu“, ne pokyn ke smazáníClient.email. Pokud potřebuješ kontakt vědomě vyčistit, dělej to přes samostatný klientský/contact edit flow. - Booking acquisition helper v
booking-acquisition.tsmapuje do stejných polí imtm_*query parametry; nové tracking integrace proto u kampaní preferuj psát tak, aby fungovaly sutm_*imtm_*bez rozdílu v admin read modelu.