UI prototype → gap list against current state and alignment plan
Reference baseline: docs/design/mockups (tokens.css is the single style source for all five clients: the default "graphite × antique gold" theme is taken from the logo #666666/#C7A674, with three switchable primary colors via data-theme="pine|cinnabar|indigo"; rice-paper background --paper, gold-amber accent --amber, serif headings --serif).
Agreed conventions:
- P0 = high-frequency core-path screens (patient records, treatment entry, global shell theme), aligned pixel-perfect first; P1 = remaining implemented screens aligned one by one; prototype only = planned features, no implementation scheduled.
- Gaps are grouped into four categories: layout / color scheme / components / copy.
- The data support column marks whether each information block in the prototype has a real interface behind it:
✅directly usable;⚠️requires frontend aggregation;❌no interface. Blocks marked❌are rendered as a "planned feature" placeholder or not rendered at all during implementation — no fake data (same warning convention as app.md). - Each screen is committed independently once done (titles in the style
feat(web): patient record aligned to prototype), with local dev server screenshots compared against the prototype images indocs/public/screenshots/before committing.
tokens single-source plan
| Client | Current state | Approach |
|---|---|---|
| web admin console | antd default blue #1890ff (config/defaultSettings.ts:12), system sans-serif | Copy tokens.css → web/src/tokens.css and import it in global.less; align defaultSettings colorPrimary with the ProLayout token to --brand; switch headings/numbers to --serif |
| desktop shell | The bootstrap page desktop/ui/index.html carries its own styles | Sync the tokens variables and serif headings |
| app mini program | Single-page index.css with scattered variables like --brand-color #c7a674 | Import tokens.css, delete the scattered variables and reference the new ones |
| Kiosk | Dark green #2f6d4f / #f6f5f1 background | The values happen to equal the pine theme → apply data-theme="pine" on the whole page, import tokens.css, replace the scattered hex values |
| tablet display screen | Dark #101613 + #ffce54 | Import tokens.css, keep the graphite base with --amber accents, replace scattered hex values |
1. Admin console web (screen by screen)
| # | Screen (prototype) | Current state | Main gaps | Data support | Level |
|---|---|---|---|---|---|
| 1 | Global shell + theme (per the appearance prototype) | ProLayout mix layout, antd blue, white background | Color: primary/selected/links all switch to the --brand family; Layout: mix → pure sidebar (layout: 'side'), white sidebar with rounded selected block; Components: logo slot replaced by a 养-character square badge + serif clinic name; Copy: Youngs.fun → clinic name subtitle | ✅ | P0 |
| 2 | Patient record (admin-customer) | Customer/Update bare ProForm with 7 fields stacked vertically | Layout: full-page rework = header card (avatar/name/level chip/meta info + actions) + 4 stat cards + left "treatment records" timeline + right "held cards" rail; Components: serif name/numbers, gold-amber level chip, timeline date blocks, remaining-visit progress bar, follow-up reminder note block; Copy: 「年纪」→「年龄」 etc. | Name/gender/age/phone/level/birthday ✅; treatment records ✅ queryTreatByPage({customerId}); cumulative spend ✅ GET /api/v1/order/summary (fetchOrderSummary, sum of completed order prices); held cards/remaining visits ✅ already implemented (customer_cards + /api/v1/card, the patient page renders the remaining-visit progress bar); next follow-up ❌ no per-patient query → "planned" placeholder | P0 |
| 3 | Treatment entry = desktop workstation (desktop-workstation) | Treat/Create single-column long ProForm (four examinations full-width textarea, five-phase nested ProCards) | Layout: patient header card + four examinations 2×2 + pulse grid for left/right hand (cun/guan/chi) + point selection left/right + diagnosis (pattern + course) + five-phase diagram + treatment plan entries + right rail (history lookup / print prescription note entry); Color/Components: card-based panels, label chips, serif section headings; Copy: labels aligned to prototype (chief complaint / inquiry / inspection / palpation / pulse / point selection / diagnosis / treatment plan) | All Treat fields ✅; patient header card ✅ fetchCustomerById; history lookup ✅ queryTreatByPage; print prescription note ⚠️ the print template pipeline (D7) can be attached; order entry/card write-off ❌ no order creation interface in the treatment flow → "planned" placeholder; scan-to-enter ✅ existing ScannerInput/scan flow | P0 |
| 4 | Daily report (admin-dash-day) | Dashboard/Day three ProCard tables | Layout: 4 stat cards on top + left staff summary table + right last-7-days bar chart + today's follow-ups/today's summary; Color/Components: status pill (submitted/not filled), reminder buttons, serif numbers; Copy: title carries the date + ‹previous day› navigation | Staff summary/today's follow-ups/today's summary ✅ three interfaces already in use; on-duty/treatment/new-patient counts ❌ no aggregation interface → deferred or "planned"; last 7 days ⚠️ aggregate by per-day treat queries (interface params support startTime/endTime) | P1 |
| 5 | Service card management (admin-items) | Item/Query ProTable (name/price/description/time) | Layout: tip banner + cover column + publish toggle column + sort column + pagination; Components: first-character cover chip; Copy: tip matches the prototype | Name/price/description ✅; cover/publish/sort ✅ the entity already has them (ItemEntity cover/enabled/sort, ItemView persists enabled=1/sort=0 by default; only the web typings.d.ts is out of sync) → P1 can genuinely implement toggle/sort/cover without placeholders | P1 |
| 6 | Order management (admin-orders) | Order/Query ProTable with status Tag in blue/orange/green/gray | Layout: tab status filter (existing valueType select → change to tabs or keep); Color: Tags switch to theme-colored pills; Copy: 「已下单」→「待接待」 to match the prototype and the Kiosk agreed policy | Order number/patient/item/amount/status/accept-complete-cancel ✅ all real | P1 |
| 7 | Advertising screen (admin-ads) | `Ads/Materials | Schedules | Screens | Calls` four ProTable pages |
| 8 | Retrospective / follow-up / staff / login | No dedicated prototype | Only passively aligned with the global shell theme (#1); no dedicated screen work | ✅ | P1 (passive) |
| 9 | Appearance settings (admin-appearance) | No such page | Create a "system/appearance settings" page: 4 theme cards to choose from → write to localStorage → runtime switch of data-theme + antd tokens + live preview area | Purely frontend ✅ | P1 |
| 10 | Appointment scheduling / Chinese medicine prescriptions / billing and settlement | None → appointments/settlements domains already built | Appointment scheduling already implemented (5910aee/f04cf62: appointments table + /api/v1/appointment + /appointment/* list workbench + /appointment/schedule physician×slot scheduling grid); billing and settlement already implemented (26d6419/9d1e018: settlements table + stored-value ledger deductions + /api/v1/settlement, /api/v1/recharge + /billing/settle settlement counter; visit card deduction 81aaf5e, receipt printing f57f10d already implemented, prescription herb lines still design-only); Chinese medicine prescriptions already implemented (f91add1/0ee1d1d: prescriptions/prescription_items tables + /api/v1/prescription + /prescription/* for writing and querying prescriptions, herbs as free text; compatibility review 7100355, pricing 60e818c, decoction-service pickup 141f295 already implemented) | Appointments ✅ / prescriptions ✅ / billing ✅ | All three are implemented; prescription inventory (herb in/out) remains planned |
2. Desktop workstation (desktop shell)
| # | Screen | Current state | Main gaps | Level |
|---|---|---|---|---|
| 1 | Treatment entry main screen | Loads the web /treat/create | Same as web screen 3; lands with P0; the ScannerInput scan-to-locate bar aligns with the page | P0 (same as web) |
| 2 | Shell bootstrap page ui/index.html | Own light styles, peripheral self-check buttons | Align colors/serif headings with tokens; unify the display style of peripheral status (receipt printer/scanner) | P1 |
3. Mini program app
Four-tab structure (Home / Appointments / Cards / Mine); page registration and tabBar in app/src/app.config.ts; the white-label self-check has been moved from Home to the bottom of the "Mine" page. The table below records "current state" per the current implementation; unimplemented parts still list gaps as before, with no fake data.
| # | Screen | Current state | Main gaps | Level |
|---|---|---|---|---|
| 1 | Home (mobile-home) | Implemented per the prototype: brand header / appointment banner (entry → pages/booking) / "today's wellness" grid (actually fetches GET /api/v1/kiosk/items) / "my cards" (entry linked to pages/card; devices that have looked up cards echo name + card count + lookup date locally for 7 days) / wellness tips; colors consolidated into tokens (src/tokens.css, same source as mockups) | No layout/color gaps; card purchase and remaining-visit details remain planned — remaining visits are only shown on the card page after verification, and the plaintext is never stored on the device | P1 (aligned) |
| 2 | Self-service booking (mobile-booking) | Implemented: date strip (7 days from today) → time-slot grid (9:00–17:00 on the hour, past slots today disabled) → item selection (actually fetches published items, "decide at store" allowed) → phone/name submission → receipt shows appointment number and slot; goes through the unauthenticated write entry POST /api/v1/kiosk/appointments; policy in app-booking design | No layout gaps; slot occupancy lookup is not implemented — the server does not query occupancy, conflicts are reconciled by staff in person (noted on the same page) | P1 (implemented) |
| 3 | Card lookup by phone (mobile-card) | Implemented: phone number + SMS verification code two-factor (POST /api/v1/kiosk/sms-code → card-lookup); weapp additionally wires wx.login → openid-session to exchange for an openid and takes the openid_only path (openid kept in memory only, silent fallback to the SMS path if the exchange fails, H5 uses SMS only); driven by the three-state switch sinomed.phone-lookup.mode, default off (disabled); every attempt including rejections lands in the card_lookup_logs audit | openid self-binding already implemented (POST /api/v1/kiosk/openid-bind; under openid_only the user proves their phone with a code then binds to the patient record, idempotent on the same record, rejected if already bound to another record; activation still lacks WeChat credentials, see the blocker section in the repo-root todo.md); no login session | P1 (implemented) |
| 4 | Mine (mobile-me) | Implemented: local appointment summary + cross-device appointment lookup (phone + verification code as a one-time two-factor via POST /api/v1/kiosk/my-appointments, returns unexpired not-yet-arrived appointments, honestly returns empty when there are none) + self-service cancellation of not-yet-arrived appointments (POST /api/v1/kiosk/appointment-cancel, re-issuing a code then cancelling; decided on the user's behalf 2026-10-16) + theme switching (rendered on H5 only) + white-label self-check at the bottom of the page | Login sessions (session persistence) are not implemented — unauthenticated phone number lookup would cause horizontal data exposure, beyond the demo MVP scope | P1 (implemented) |
4. Kiosk
| # | Screen | Current state | Main gaps | Level |
|---|---|---|---|---|
| 1 | Service selection (kiosk-menu) | The menu page already has a service list + shopping basket structure | Colors are already dark green (=pine) → switch to tokens variables to consolidate scattered hex; serif headings, rice-paper background, gold-amber price tags/accents; basket/checkout button details aligned to the prototype | P1 |
| 2 | Order receipt (order) | Exists | Aligned globally per above | P1 |
5. Display screen tablet
| # | Screen | Current state | Main gaps | Level |
|---|---|---|---|---|
| 1 | Carousel + queue calling (tablet-screen) | The player carousel + call queue-calling bar are implemented | Dark background switched to the tokens graphite base, h1 em in gold-amber, serif headings, 王* masking on the queue-calling bar (implemented, to be verified); page number/progress details | P1 |
Execution order
- P0 web global shell theme (tokens.css landed in web + colorPrimary/ProLayout aligned) → 2. P0 patient records → 3. P0 treatment entry (including the desktop workstation main screen) → 4. P1 orders → 5. P1 service cards → 6. P1 daily report → 7. P1 advertising screen → 8. P1 appearance settings (theme switching implemented) → 9. P1 Kiosk → 10. P1 tablet → 11. P1 app → 12. P1 desktop bootstrap page.
Per screen: change the implementation → pnpm build/lint passes → local screenshots compared against prototype PNGs → independent commit (title in the style feat(web): patient record aligned to prototype, body listing layout/color/components/copy changes with evidence) → fetch+rebase → push.
Progress
- [x] P0-1 web global shell theme alignment (bd72bae)
- [x] P0-2 patient record alignment (fc84cfa)
- [x] P0-3 treatment entry (desktop workstation main screen) alignment (42e567b; the desktop shell shares the page)
- [x] P1-4 order management alignment (f42e0ee)
- [x] P1-5 service card management alignment (8d4b77f)
- [x] P1-6 daily report alignment (1c72d5b, same batch as fix(server): paginated date filtering switched to Date comparison b8dc2a8)
- [x] P1-7 advertising screen four pages aligned (bcb6b1c; four pages merged into one page with 4 tabs + right rail, old routes redirected)
- [x] P1-8 appearance settings (8591447; runtime theme switching: data-theme CSS variables + useAntdConfigSetter injecting a plugin into ConfigProvider + settings.colorPrimary/sidebar tokens, delivered in three layers)
- [x] P1-9 Kiosk alignment (eccf091; tokens.css copied from the same source + index.html mounting data-theme="pine"; hero/three-step capsules/gradient initial-character cards/two-row basket aligned to the kiosk-menu prototype, all scattered hex values consolidated into variables)
- [x] P1-10 tablet display screen alignment (2caa81f; tokens.css copied from the same source; graphite base --ink, paper-colored rounded slide cards with real materials embedded, pager dots showing real progress, queue calling changed from a full-screen overlay to the prototype's bottom bar = gold-amber label + masked number + serif room name, auto-recesses after 15s)
- [x] P1-11 app mini program Home alignment (9566fdb; mobile-home layout: brand header/appointment banner/today's wellness/my cards/wellness tips/white-label self-check at the page bottom; the grid actually fetches published items, appointment and card remaining visits honestly shown as "planned feature" pills+note; tokens.css imported from the same source, --brand-color white-label injection retained)
- [x] P1-12 desktop shell bootstrap page alignment (5e656b7; grep re-check confirms zero residual scattered colors across all five clients, --brand-color white-label injection mechanism retained)
- [x] P1-13 prescription compatibility review + herb dictionary/pricing (8e3402f/0144ba0; eighteen incompatibilities/nineteen antagonisms static rule hints without blocking; herbs dictionary + real-time prescription trial calculation, unlisted herb names are not billed)
- [x] P1-14 card entity (visit card) and settlement deduction (81aaf5e; customer_cards + /api/v1/card for issuing/deactivating cards, payType 5 deducts 1 visit with 0 actual income; the patient page's card rail and the settlement counter's visit card option implemented)
- [x] P1-15 settlement receipt printing (f57f10d/1b33703;
GET /api/v1/print/receipt/{settlementId}server-side templated 80mm HTML: order number S+settlement id, the visit card deduction amount column shows 「次卡抵扣」, names HTML-escaped; the settlement counter offers a print entry after successful payment and recent settlements can be reprinted; templates can still be overridden via data/printtemplates) - [x] P1-16 prescription decoction-service pickup (141f295/47e4828; prescriptions +decoction_status/decoction_bags, prescriptions with decoction=true set bag count = number of doses and land in 「待煎」;
PUT /prescription/{id}/decoction?status=moves one-way from 待煎→可取→已取, regressions/skips/no decoction selected → 400; service fee ¥3/bag computed in real time and shown as a hint only; the prescription page has a checkbox, the query page a status column + drawer flow buttons; rush and pickup window not implemented — demo scope) - [x] P1-17 prescription herb lines entering settlement — ruling: not implemented (based on the existing in-repo ruling "billing is based on service card order settlement": prescription pricing is a trial-calculation/hint policy, not a billing source — see the settlement section in api.md, the note on the prescription page, and the PrescriptionController javadoc, since 60e818c and reiterated by 141f295). The prototype billing's herb line (「8 味 × 7 剂 ¥280」 next to tuina) is illustrative, not an interface contract; when it conflicts with a ruling, the ruling prevails. Technical evidence: orders.price is integer yuan while prescriptions are priced per unit, so conversion to yuan necessarily rounds, violating "money matters must be exact"; multi-line settlement (settlement_items) is a domain-level change. The decoction fee ¥3/bag follows the same policy (hint only).
- [x] P1-18 staff leave completed (c33f1cf/7e8a7a0; web.md leftover "/staff/leave submission calls the staff creation interface, feature incomplete": the original server
POST /staff/leavewas a placeholder returning an empty object and the web page's misuse of createStaff would turn a leave submission into staff creation — rebuilt: staff_leaves table +/api/v1/staff/leave/{,page,id}for submission/pagination/deletion (type 0 sick leave 1 personal leave, reason, start/end time validation, joins in staff names), web registration form + recent leave list; policy: record-only, does not change login status, no approval flow (demo scope)) - [x] P1-19 condition prescription templates (6b5d992/a6b0ab7; aligning with chinese_medicine_store_cos's condition prescription library approach: prescription_templates + items tables,
/api/v1/prescription/template/{,page,enabled,id}CRUD + publish filtering (deactivated ones don't appear on the prescription page), condition names unique 1–20 characters, templates store only suggested values (doses/decoction/usage/notes), updates replace all herb lines in one pass (sort=line order); web/prescription/templatesmanagement page (inline add/remove of herb lines, publish Switch, delete Popconfirm) + the prescription page's "apply template" dropdown brings everything in one click, freely editable after and never written back to the template; seeds 风寒感冒/脾胃虚弱 as two examples, HTTP tests covering CRUD/duplicate names/validation/unauthenticated 401) - [x] P1-20 tuina/moxibustion treatment-course card write-off ledger (9e5a379/2205c6c; reusing the P1-14 visit card chain policy — tuina/moxibustion treatment-course cards are service cards counted by course +
customer_cards, this change fills the missing write-off dimension in course management:card_usagesappend-only ledger, settlementpayType=5deducts a visit and records "which visit/serving employee (from the order's staffId)/order number" in the same transaction, rolls back with the settlement, no manual deduction;GET /api/v1/card/{id}/usagesreturns newest first with staff names joined; web patient record card rail shows "used X/N times" + a write-off record popup; seed demo cards get 3 historical write-offs (backfilled into the timeline, old cards backfilled without order numbers per policy); HTTP tests covering issue card→create order→visit card settlement→query ledger plus no-card 400/unknown card holding 400/unauthenticated 401) - [x] P1-21 paste prescriptions and pickup flow (2134975/280c435; prescriptions gains
prescription_type(0 decoction default/1 paste),paste_status(paste prescriptions land in 1 待制作),craft(finishing method, paste only), column definitions with default 0 so legacy insert points stay unchanged;POST /prescription/writes paste prescriptions priced per ingredient without bag counts (mutually exclusive with decoction service, each with its own pickup chain);PUT /api/v1/prescription/{id}/paste?status=moves one-way from 待制作→可取→已取, non-paste → 「不是膏方」400, regression/skip → 「流转无效」400, same shape as the decoction flow but not mixed with it; web prescription page type Radio (decoction/paste, switching to paste clears decoction, switching back clears the finishing method) + finishing method Select (炼蜜/清膏/糖膏/阿胶收膏), query page list type/paste status columns + detail drawer with a "制成 · 转可取 / 顾客已领取" button; seeds add a paste example (nine herbs, 30 doses priced per ingredient, 炼蜜 finishing, lands in 「可取」 leaving 「已取」 for the demo); 5 unit tests + HTTP tests covering write paste→flow→decoction 400/skip 400/unauthenticated 401) - [x] P1-22 patient medical history (allergy/past history) (7bb2679/bfcf3e8; the features.md electronic medical record row's gap "no independent history structure":
customer_historiesappend-only table (type 0 allergy/1 past, content ≤200 characters),GET/POST /api/v1/customer/{id}/history+DELETE /api/v1/customer/history/{id}, guards for nonexistent patient/invalid type/blank content/overlength all return 400 (content is trimmed before storage, nothing is persisted when validation fails); no edit or audit trail — demo scope, delete means delete; web patient record history rail (allergy/past Radio.Button + input + records, inline type pill/content/date/delete confirmation); seeds give two demo patients one allergy and one past history each (backfilled into the timeline 28/28/9 days ago); HTTP tests covering record two→newest first→delete one and the four guards/unauthenticated 401; a walkthrough fix on the way: the paste seed missed settingdecoction_status(NOT NULL column without default) causing the demo environment's paste seed to fail silently) - [x] P1-23 prescription allergy review (6c354cc/7237b2f; the allergy dimension of the features.md compatibility-review row:
POST /api/v1/prescription/allergy-checkcompares againstcustomer_historiestype=0 allergy history, a hit occurs when the record's raw text contains the herb name — only terms ≥2 characters participate, single characters like 「参」 avoid false positives on long texts; one herb hitting multiple histories reports one each, one history hitting multiple herbs reports one each; past history does not participate; unknown patient/empty list → 400; hints do not block, same policy as the compatibility review; the web prescription page debounces 500ms after locating the patient and checks as you type, the hit block lists herb + history raw text, no placeholder when there's no hit; seed 王慕清「阿胶、蜂蜜过敏」× paste containing 阿胶 hits naturally in the demo; 4 unit tests + HTTP tests covering create record→add history→hit and guards/unauthenticated 401) - [x] P1-24 monthly billing report (a2f695a/e5e4eea; the features.md report row's gap "no business/billing reports":
GET /api/v1/settlement/report/month?month=yyyy-MMfetches the month's settlements by settlement time and aggregates in Java (the SQLite dialect does no date grouping) —days[]lists per-day order counts/actual income only for days with settlements,payTypes[]payment method composition in ascending payType with labels, visit card write-offs (payType 5, actual income 0) in a separatecardCount, stored-value top-ups not counted as revenue; invalid month format/nonexistent month → 400, unauthenticated → 401; web/dash/month(month picker + three cards for monthly actual income/settlement count/visit card write-offs + payment method composition + per-day details + policy note); seeds add 6 settlements this month via 微信/支付宝/现金/储值 (backfilled into the timeline by day, stored-value following the same policy as negative ledger entries) + 1 visit card write-off (the 4th visit with an order number, deducting a visit, consistent with the P1-20 write-off ledger policy); 2 unit tests + 2 HTTP tests; a real-device walkthrough for 2026-10 reported 7 orders ¥3516, 1 visit card write-off, tuina card remaining 6/10 matching 4 write-off records, and a same-restart re-verification found no duplicate seeds) - [x] P1-25 formula library (07832e5/cfb32fa; the features.md herb/formula row's gap "formula library and pinyin search not done", aligning with his_mvp's formula library + pinyin input:
formulas+formula_itemstables (unique formula names 1–20 characters,pinyinfull-spelling lowercase lookup code, source/indications; herbs same as prescriptions herb/weight/special/sort),/api/v1/formula/{,page,id}CRUD +keywordfuzzy match on formula name or pinyin code (like concat('%',:kw,'%'), an empty keyword from the service matches all); guards for empty name/over 20 characters/duplicate name (update exempts itself)/empty pinyin/not lowercase letters (entry auto-lowercases)/at least 1 herb/empty herb name/dose>0/nonexistent formula all return 400; updates replace all herb lines in one pass; division of labor with condition templates — formulas store only literature reference values without doses/decoction suggestions; web/prescription/formulasmanagement page (name+pinyin two-line rows, herb chips, name/pinyin search) + the prescription page's "formula library" dropdown search brings out the whole formula in one click (herbs carried over, dose count unchanged, freely editable after and never written back); seeds with 9 classic formulas (四君子汤/四物汤/逍遥散/补中益气汤/玉屏风散/六味地黄丸/平胃散/二陈汤/八珍汤, with sources and effects) + 10 herbs added to the dictionary (early-return guard removed, existing databases backfilled incrementally by existsByName); 5 unit tests + HTTP tests covering create→search→edit→delete and guards/unauthenticated 401) - [x] P1-26 acupoint dictionary (9ecc30d/b8247ca; the features.md acupuncture/point-selection row's gap "no acupoint dictionary":
acupointssingle table (unique acupoint names 1–10 characters,pinyinfull-spelling lowercase lookup code,meridianrequired, location/indications optional),/api/v1/acupoint/{,page,id}CRUD +keywordfuzzy match on acupoint name or pinyin code; guards for empty name/over 10 characters/duplicate name (update exempts itself)/empty pinyin/not lowercase letters/empty meridian/over 20 characters/nonexistent acupoint all return 400; the treatment record's point selection field stays free text — the dictionary only assists point selection; web/treat/acupointsmanagement page (name+pinyin two-line rows, meridian pills, name/pinyin search) + the treatment page's left/right point selection rows each gain a "dictionary point selection" dropdown (search by name/pinyin, selections appended with 「、」into the point selection text, still freely writable); seeds with 27 common acupoints (足三里/三阴交/合谷/太冲/内关/外关/曲池/阳陵泉/委中/百会/风池/大椎/关元/气海/中脘/神阙/涌泉/太溪/血海/阴陵泉/肩井/命门/肾俞/脾俞/胃俞/印堂/太阳, with meridian/location/indications per point); 5 unit tests + HTTP tests covering add→search→edit→delete and guards/unauthenticated 401) - [x] P1-27 recurring staff shift table (b684f78/0be5ae0; the features.md scheduling row's gap "recurring staff shift table not done":
staff_shiftssingle table (staff_id +weekday1 Monday…7 Sunday +start/endHH:mm, unique per staff per weekday, cycling weekly),/api/v1/shift/{,page,staff/{staffId},id}CRUD + pagination with optionalstaffId/weekdayfilters (the service dispatches derived queries by null value to avoid the SQLite JPQL null-parameter pit, ordered by staff+weekday+time); guards for empty staff/nonexistent staff/out-of-range weekday/time not HH:mm (hour≤23 minute≤59)/start ≥ end/duplicate staff+weekday (update exempts itself)/nonexistent shift all return 400, responses join in staff names and weekday labels; shift table policy: scheduling reference only, attendance check-ins still follow signs, no write-back to check-in records, no linkage with leave/appointments for now; web/staff/shiftsweekly matrix page (rows=staff, columns=Mon~Sun, slot times clickable to adjust,✕Popconfirm delete, "add shift" popup with staff/weekday/TimePicker 30-minute steps + off-work after on-work prevalidation,canAdminonly); seeds with 沈知远 Mon–Fri 09:00-18:00, 苏文若 Mon–Sat 09:30-18:30, 顾景明 Wed–Sun 10:00-19:00 for 16 entries total; 5 unit tests + HTTP tests covering add shift→pagination filter→weekly list→edit→delete and guards/unauthenticated 401) - [x] P1-28 acupuncture prescription records (2099b39/9d282af; the features.md acupuncture/point-selection row's gap "acupuncture prescription (needle method/retention/manipulation records) not done": treatment
treatsgains 4 columns (acu_methodneedle method ≤30 characters,retention_minutesretention minutes 1-240 optional,manipulationmanipulation ≤30 characters,acu_coursecourse/frequency ≤100 characters, blank text treated as empty), saved in full with treatment creation/updates (TreatView bidirectional mapping, unified validation in service save/update, out-of-range 400); web treatment page "acupuncture prescription" section (needle method/retention InputNumber 1-240/manipulation/course TextArea, refilled on revisit); seeds: the insomnia record gets 毫针+耳穴压豆/25 minutes/平补平泻/2×/week for 2 weeks, the back pain record gets 温针/20 minutes/补法/twice a week×3 (written only when the needle method is empty, idempotent); TreatUpdateTest extended with 2 cases (field round-trip/guards) + new TreatAcuHttpTest with 2 cases (create→read back→update→clear, guards, unauthenticated 401)) - [x] P1-29 weekly shift vs. attendance comparison (e4af672/afd2a7d; the features.md scheduling row's gap "shift table and attendance linkage not done":
GET /api/v1/staff/timesheet/week?weekDate=anchors the input to its week's Monday (rolling back(dow-2+7)%7days), pulls the week's signs using the sameTimesheetUtilpolicy as monthly attendance (earliest on-work/latest off-work of the day, rounded to whole hours, historical days missing an off-work punch count 0), staff fully as rows by ascending id × 7 days, shift plans compared by weekday (blank when no shift/no punch); emptyweekDatetakes the current week, invalid format reports 400; along the wayDateUtil.parseParamis switched to strict mode (out-of-range dates like2026-13-40no longer roll over into valid dates); web/staff/timesheet/weekstaff × 7-day comparison board (expected/actual two rows, pick a date to see its week + back to current week,canAdminonly); StaffWeekTimesheetTest 3 cases + StaffWeekTimesheetHttpTest 2 cases (structure reading/400/401)) - [x] P1-30 herb in/out stock and expiry alerts (the features.md herb inventory row's gap "inventory not done":
herb_stock_logssingle-table ledger (herb_id +type1 in/0 out +quantityin grams + batchexpiry+supplier≤50 characters +note≤100 characters), no balance column — stock is recomputed from the ledger = total in − total out, outbound consumes batches first-expired-first-out (FEFO),GET /api/v1/herb-stock/balance?expiryWithinDays=gives each herb's current grams + earliest unconsumed batch expiry + days to expiry +warnExpiry(default 30 days, cap 365, expired also alerts);/api/v1/herb-stock/{,/page,/balance,id}registration/ledger pagination (optional herbId/type filters, id descending, herb names joined)/balance/delete (demo scope, no audit trail); guards for missing herb/nonexistent herb/invalid type/non-positive quantity/expiry not yyyy-MM-dd or earlier than today/overlong supplier and note/outbound exceeding current stock all return 400; web/prescription/herb-stockbalance dashboard (threshold 30/60/90 switching) + ledger registration popup (outbound without expiry) + ledger pagination delete; seeds: 5 common herbs inbound (黄芪 5000/当归 3000/茯苓 4000/白术 2500/甘草 6000 grams, all with batch expiry and supplier) + 1 outbound of 甘草 800 grams (clinic requisition), 白术 expiry 25 days serves as the 30-day threshold alert example; HerbStockTest 3 cases + HerbStockHttpTest 2 cases. Ledger scope — no purchase documents, stocktaking, or settlement involved, and prescriptions still don't deduct stock) - [x] P1-31 external therapy prescription templates (the features.md acupuncture/point-selection row's gap "ear point/plaster and similar external therapy templates not done":
therapy_templatessingle table (unique template names 1–20 characters,methodexternal therapy method required ≤10 characters,targetsuggested points/body area +coursesuggested course/frequency +noteprecautions each ≤100 characters optional,enabledpublish flag),/api/v1/therapy-template/{,enabled,page,id}CRUD + pagination with optionalnamefuzzy /methodexact filters (Specification conjunction, avoiding the SQLite derived-query null-parameter pit); guards for empty name/over 20 characters/duplicate name (update exempts itself)/empty method/over 10 characters/overlong optional sections all return 400; same policy as condition templates — suggested values only, after applying on the treatment page they can be freely edited with no write-back to the template, and deactivated ones don't appear on the treatment page; web/prescription/therapy-templatesmanagement page (method chips, publish toggle) + the treatment page's acupuncture prescription section gains an "apply external therapy template" dropdown (method/course refilled, point suggestions appended with 「、」into point selection-left); seeds with 4 common external therapy combinations (失眠·耳穴压豆/咳喘·穴位贴敷/肾阳虚·温灸关元/腰痛·留罐); TherapyTemplateTest 3 cases + TherapyTemplateHttpTest 2 cases (create→list→pagination filter→edit→delete and guards/unauthenticated 401)) - [x] P1-32 pharmacy and dispensing flow (the features.md pharmacy/dispensing row's gap "not implemented", per the finalized policy in dispensing.md:
prescriptionsgainsdispense_status(0 待发药 default/1 已发药 one-way; NOT NULL column, both the service save and the seed creation points explicitly set 0)/dispense_time/dispense_bycolumns +dispense_logsone-way ledger (prescription_id indexed, action=1, staff_id, note ≤100 characters);POST /api/v1/prescription/{id}/dispensemoves 待发药→已发药 (duplicate → 400「处方已发药」, nonexistent → 400, operator taken from the current logged-in account, note ≤100 characters);GET /api/v1/prescription/dispense/pageworkbench pagination (optional dispenseStatus/customerKeyword filters, id descending, joins in patient name/phone plus type/dose count/herb count/decoction-paste labels); decoction service/paste advancing to 「已取」 automatically sets 已发药 and writes a ledger entry (operator left blank, note records the source — the deviation from the finalized "take the operating user" is recorded in the doc); web/prescription/dispensarypharmacy workbench (three-state switching with counts, patient search, dispensing popup with optional note and one-way confirmation, dispensed rows read-only) + the prescription query drawer's dispensing status and quick confirmation; seed dispensing demo (first prescription set to dispensed + ledger backfilled into the timeline); DispenseTest 5 cases + DispenseHttpTest 1 case (dispense→pagination filter→duplicate 400→unauthenticated 401)) - [x] P1-33 refunds and daily reconciliation (per the finalized policy in reconciliation.md:
refund_logssingle table (settlement_id unique index — one refund per order, order_id indexed, money=original settlement actual income (visit card 0), pay_type denormalized, staff_id optional, reason ≤100 characters required);POST /api/v1/settlement/{id}/refundrefunds the whole order back to its original channel — stored value restores a positive ledger entry (recharges+notecolumn writes 「退单 #结算id」), the visit card voids that order'scard_usagesand restores the visit count, 微信/支付宝/现金 only write a refund record; the order2→1can be re-collected;GET /reconciliation?date=aggregates three segments by natural day (payments/top-ups/refunds +netMoney,cashNetMoney), the top-up segment only counts real top-ups (positive, non-refund notes — stored-value payment deductions count on the payment side, refund restorations on the refund side); the settlements.order_id unique index is removed with the "re-collect after refund" policy (SettlementIndexMigration drops the legacy index from old databases on startup, and settle's duplicate-prevention is changed to "most recent unrefunded"); web/billing/reconciliationdaily reconciliation page (stat cards + payment composition + top-up/refund detail tables) + the settlement counter's row-level "refund" on the most recent settlement (reason popup with one-way confirmation, refunded tag); seeds refund one historical cash settlement for the demo (order returns to the queue and can be re-collected); RefundReconciliationTest 6 cases + RefundHttpTest 2 cases (refund→reconciliation→duplicate 400→missing/invalid date 400→unauthenticated 401), real-device verification of stored-value refund restoring the balance, re-collection landing a new settlement, and the three-collection block) - [x] P1-34 automatic queue calling on arrival (the "automatic queue calling on arrival number" gap in docs/tablet.md, approved 2026-10-12: appointment check-in 0→1 makes the server automatically write
queue_callsand the front desk's manual queue calling run in parallel — number = that day's check-in sequence zero-padded to two digits (natural day of check-in time, sequenced by id), room = treating physician's name + 「诊室」 / 「待分配诊室」 when unassigned, name masked (张*), broadcast to all screens; skipped/cancelled numbers are not called; walk-ins without appointments still go through manual queue calling. Landed in AppointmentServiceImpl.fireQueueCall + QueueCallEntity comment refresh, with the regression AppointmentSlotTest.receiveFiresQueueCallAndNoShowDoesNot (skipped numbers not called/one call per check-in/two-digit number/room from physician name/name masked/full-screen broadcast/unassigned room)) - [x] P1-35 mini program four-tab status addendum (the §3 table rewritten per current implementation: Home's "my cards" changed from a "planned" placeholder to a real entry + local summary, and the appointment/cards/mine screens added to the current-state table from the prototype list; remaining planned items honestly left blank = card purchase and remaining-visit details, patient openid binding registration, login session. Corresponding implementation
9cfaa92/be5da27/af0af7f)