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