Codebase đã được chẩn đoán. Quyết định đang chờ.
Báo cáo kiến trúc và impact analysis trên 2.470 file JS/TS với 9.445 cạnh phụ thuộc đã hoàn tất. Hai hướng đi đang trên bàn — sửa stack hiện tại trong 9-12 tháng, hoặc viết lại native Android trong 10-12 tháng cho SUNMI D3 Pro.
NailSoft / Harmony
Target · SUNMI D3 Pro
Keep-stack hay Native rewrite?
Quyết định này ảnh hưởng 9-18 tháng tới. Keep-stack giữ được iOS parity và Salon/Retailer cùng một codebase. Native rewrite cho D3 hiệu năng cao hơn nhưng phải maintain hai app trong thời gian dài.
Bắt đầu từ đâu?
Cả hai phương án đều khuyên: không động vào store root (610 files) hay shared/utils (813 dependents) trước. Bắt đầu từ Signal runtime (26 files) hoặc Foundation + parity tests, rồi mới đến Calendar.
- SUNMI D3 hardware chưa được tối ưu — JS work, render churn, Android scroll/gesture là bottleneck thật, không phải iOS smooth-scroll.
- Realtime + heavy invalidation — SignalR events gọi thẳng vào layout recompute trong giờ cao điểm.
- Hub files —
shared/themes(817 dependents),utils/index(813),resources(499) khuếch đại blast radius cực nhanh. - JSON.stringify memoization ở hot paths là dấu hiệu unstable data shape.
- Backend bottleneck (rewrite path) — chỉ 1 BE engineer, BFF/API contract chậm là FE chờ mock dài.
Tuần 1-3: Build dependency baseline + parity test framework + D3 benchmark harness. Đây là việc giá trị bất kể chọn phương án nào — keep-stack hay rewrite đều cần parity gates và performance evidence để gate từng phase.
Mỗi module = một cluster. Đo đo đo trước khi đụng.
Đo bằng yarn graph:impact ở depth-3 trên các seed file đại diện. Diện tích thật của một thay đổi là tổng số file/dòng nó kéo theo, không phải số file Claude/dev sửa trực tiếp.
Depth · 3
Run · 19 May 2026
Số dependents trực tiếp. Một thay đổi ở đây = invalidate gần như cả app.
| File | Dependents |
|---|---|
src/shared/themes/index.js | 817 |
src/utils/index.js | 813 |
src/resources/index.js | 499 |
src/shared/providers/ModalProvider.js | 183 |
src/redux/slices/index.js | 164 |
src/apis/index.js | 155 |
Khi cluster A và cluster B chia sẻ nhiều file, không thể refactor riêng một bên.
| Cặp domain | Files chung |
|---|---|
| Calendar ↔ Checkout | 340 |
| Calendar ↔ Payment | 340 |
| Modal/Shell ↔ Checkout | 331 |
| Checkout ↔ Printer | 236 |
| Checkout ↔ Customer | 200 |
| Printer ↔ Customer | 191 |
Tách công việc theo màn hình sẽ ước lượng thiếu. Tách theo vertical operating flow mới an toàn. Đường quan trọng nhất không phải "màn Calendar" mà là Calendar → Appointment Detail → Checkout → Payment → Receipt/Printer.
- Signal runtime (26 files) — small blast radius, unblocks Calendar batching
- Data layer adapters (35 files) — RTK Query policy + adapter helpers
- Performance harness — không phải file, là tooling foundation
- Calendar (344) — phase 2, sau Signal runtime
- Payment (343) — phase 4, sau Checkout
- Printer/Receipt (248) — phase 5, isolation
- Customer (206) — overlap với Checkout
- Store/state root (610) — sau khi domain boundaries ổn định
- Modal/Shell (494) — global interaction layer
- Checkout/Basket (472) — phase 3, sau Calendar
- Hub files — chỉ thay vì incremental adapter pattern
Tám phát hiện. Mỗi cái có cost cụ thể.
Audit Calendar — màn hình hot nhất của Salon POS — bằng cách đọc code, git history và stack profile. Không phải tất cả đều phải sửa cùng lúc; cần ưu tiên theo cost-on-D3.
File · calendar slice + helpers
Target · SUNMI D3 Pro
updateStaffLayout → calculatorColumnStaffLayout bị trigger bởi 9 action: setCalendarDataByDate, removeStaff, updateBlocksBookingOnline, cancelAppointment, moveAppointmentToWaiting, updateAppointmentFromSignal, addAppointment, updateBlockTimes, updateTurnForStaffs. Mỗi sự kiện local kéo theo layout work của cả màn.
getStaffsAndBlockTimesDisplay map staff × filter blockTimes mỗi lần. getStaffDisplayAndAppointmentCount deep-clone bằng JSON.parse(JSON.stringify(staffs)). Vừa tốn CPU vừa tạo object reference mới → render churn cấp số nhân.
useScrollSync. Hook quản lý leader/follower, animated refs, shared values, snapping, momentum end, programmatic scroll, iOS smooth-scroll edge cases. Đúng vấn đề, nhưng giải bằng cách add control logic.
591c89b39, 3751afdb7, 26f5a79e8, e39352391 + spec 002-ios-calendar-scroll-stability đầu tư mạnh vào iOS. Hữu ích — nhưng không phải bottleneck của D3. D3 cần JS work, render churn, Android scroll/gesture.
CalendarHeaderStaffItem so sánh prev/next item bằng JSON.stringify. PanGestureContainer stringify reset params. useCalendarVerticalScroll, useHandleSignalCalendar stringify workingTimes trong deps. Giảm identity churn — nhưng che dấu data shape không ổn định.
useHandleSignalCalendar.js xử lý 10+ event types: appointment_add/update/checkout, change_item, staff_position, update_waiting, turn_update, staff_update_block_booking_online, edit_blocktime_updatecalendar, caller events. Mỗi event dispatch vào calendar slice action có thể recompute layout — giờ cao điểm có thể tự thấy lag dù user không thao tác.
BackgroundLayer dùng Skia Canvas/Line/Rect. DefaultListProps.CALENDAR + CalendarBody bật shouldRasterizeIOS, renderToHardwareTextureAndroid. Có thể giúp specific cases — nhưng trên Android POS hardware có thể tăng texture memory pressure. Phải verify trên D3, không assume tốt.
- Dùng
FlashListthayFlatListcho horizontal lists lớn - Tạo
useScrollSyncđể control header/body synchronization explicit - Split calendar fetch thành nhiều phase trong
calendar.jssaga - Dùng
InteractionManager.runAfterInteractionsđể defer dispatch - Giảm callback identity churn ở hot scroll code
- Spec-driven approach:
specs/002-ios-calendar-scroll-stability/có thinking rõ ràng
- Layout work bị trigger từ Redux actions thay vì batched ở UI layer
- Selectors deep-clone và rebuild quá thường xuyên
- SignalR events đi thẳng vào layout recompute — chưa coalesce
- iOS smooth-scroll polish > Android stability fundamentals
JSON.stringifydùng làm memoization hack thay vì sửa data shape- Calendar layout engine chưa được tách thành module riêng
Sửa stack đang có, hay viết lại Android native?
Cả hai con đường đều khả thi. Nhưng chúng tối ưu cho mục tiêu khác nhau. Quyết định phụ thuộc vào việc team coi iOS parity và Salon/Retailer unified codebase quan trọng đến đâu so với D3 hiệu năng top tier.
Team · 3 FE + 1 BE
Target · SUNMI D3 Pro
Giữ React Native, sửa kiến trúc tại chỗ. 6 phase, ưu tiên Calendar và Checkout boundaries.
Pros
- Salon + Retailer cùng codebase
- iOS parity giữ nguyên
- Không cần parallel maintain hai app
- Code/test/release pipelines hiện có
- Incremental — gate sau mỗi phase
- Reuse Caller, Customer, Inventory logic
Cons
- D3 vẫn chạy React Native bridge
- Performance ceiling thấp hơn native
- Store root (610) khó đụng triệt để
- Phải maintain compatibility paths lâu
- Refactor stretch dễ stall ở phase 3-5
When to pick
Khi iOS parity và Salon+Retailer unified codebase đáng giá hơn 30-50% performance gain trên D3. Khi team không muốn maintain hai app trong 1+ năm.
Kotlin + Compose + BFF. Android-first, D3-only cho v1. 7 phase, parity gate ngặt.
Pros
- D3 native performance ceiling cao nhất
- Compose UI + Skia trực tiếp
- Coroutines + Flow + Room đơn giản hơn Redux/saga
- BFF chuẩn hóa contract → backend rõ ràng
- Bỏ được tech debt cumulative
- MVI explicit hơn implicit Redux mess
Cons
- Maintain hai app trong 10-12+ tháng
- iOS không được benefit
- Retailer mode out of MVP
- 1 BE engineer = backend bottleneck
- Hidden business rules ở RN dễ miss
- Phải tự build lại Caller, Customer, modal flows
When to pick
Khi D3 là target chính, Retailer mode có thể defer, và team OK maintain hai app trong 1+ năm. Khi product muốn reset tech debt thay vì incremental cleanup.
Foundation work của 2 phương án overlap đáng kể: parity matrix, performance harness, benchmark fixtures, realtime event contract. Không cần quyết định strategy ngay tuần 1 — có 3-4 tuần foundation work có thể bắt đầu trước.
Timeline ở mức tuần. Phase gate, không deadline cứng.
Cả hai roadmap đều dùng phase gates thay vì calendar deadlines. Một phase không pass gate = không qua phase tiếp theo. Số tuần là range, dùng upper bound để vẽ.
Gating · phase exit criteria
Updated · 19 May 2026
- P1. Performance baseline + parity gates + feature flags +
test:calendar,test:checkout - P2. Calendar state/projection normalized + event coalescing + Android scroll authority đơn giản hóa
- P3. Basket model normalized + selection state tách + refresh/invalidate rules isolated
- P4. Payment result contract định nghĩa + rules tách khỏi checkout widgets
- P5. Receipt VM + printable payload + driver adapters tách rời
- P6. RTK Query migration, shared/hooks cleanup, Customer/Inventory boundaries
- P0. Parity matrix tất cả flow + benchmark fixtures + BFF/API contract drafts
- P1. App login + API + realtime + benchmark harness chạy trên D3
- P2. Calendar visibly + measurably faster than old app · drag/drop MVP · handoff to Appointment Detail
- P3. Calendar → Checkout → Calendar roundtrip không lose/corrupt state
- P4. Payment complete → receipt → print pass on real D3 hardware
- P5. Một store hoạt động được một ngày Salon POS trên D3 staging
- P6. Không còn P0/P1 parity gap nào với Calendar, Checkout, Payment, Printer
Bao nhiêu người, bao nhiêu tháng, đổi lấy gì?
Estimate dựa trên impact analysis thật + parity gates. Phân công team theo vertical operating flow, không theo screen — vì overlap giữa các domain quá lớn.
Senior level
Full-time on project
- Calendar surface (Compose hoặc custom View/Canvas)
- Projection/layout engine
- Drag/drop + gesture system
- Signal event application to UI
- D3 performance benchmark hệ thống
- Basket domain (MVI state + invalidate rules)
- Service/staff/category selection
- Payment rules (cash/card/split/tip/redeem)
- Checkout/payment handoff orchestration
- App shell + navigation + session
- Room/cache infrastructure
- Printer/receipt (SUNMI/D3 + Epson/Star adapters)
- Dialog/workflow system + design system
- Aggregate APIs (Calendar, Checkout, Customer)
- Normalized backend response shape
- Payment/receipt contracts
- Realtime event contracts
- Compatibility với backend hiện tại
1 BE engineer là single point of failure. BFF/API contract chậm = FE chờ mock dài hơn = parity risk tăng.
| Scope | Phương án | Team | Thời gian | Ghi chú |
|---|---|---|---|---|
| Performance rescue + architecture foundation | A | 2-3 senior | ~3 tháng | Narrower scope, không full gap closure |
| Foundation + Calendar + Signal + Checkout + part Payment/Printer | A | 2-3 senior | ~6 tháng | Realistic minimum viable |
| Toàn bộ gaps (Customer, Inventory, Settings, Retailer cleanup) | A | 2-4 engineers | 9-12 tháng | Stronger sense of "fix everything" |
| Android D3 Salon core MVP | B | 3 FE + 1 BE | 8-10 tháng | Calendar, Checkout, Payment, Receipt, Caller, Customer basic |
| Production-ready Android replacement (Salon core) | B | 3 FE + 1 BE | 10-12 tháng | Pilot + cutover + parity report |
| Broad Android parity với most old-app behavior | B | 3 FE + 1 BE | 14-18 tháng | Bao gồm Retailer, Reports, deep Settings |
| Full parity including Retailer + Reports + deep Settings | B | 3 FE + 1 BE | 18+ tháng | Marketing flows, advanced inventory |
Governance + tests + baseline + performance harness
State normalization, projection engine, scroll authority
Event queue + batching, đi cùng Calendar
Selection ≠ basket state, refresh rules isolated
Result contract, rules tách khỏi widgets
VM/payload/driver split + caller lifecycle
RTK Query migration, domain hooks move-out
9-12 tháng với 2-4 engineer, có cushion cho discovery
Impact analysis CLI + Calendar refactor slice template
Hai thứ làm việc với bất kể phương án nào: bộ command graph:* để đo blast radius trước khi đụng, và template refactor slice cho Calendar (đã pre-filled từ impact analysis).
Template · refactor-calendar-slice
# Quét toàn bộ src/, tạo graph snapshot
yarn graph:build
Chạy 1 lần khi bắt đầu, hoặc khi merge branch lớn. Lưu vào .graph-cache/.
# Default depth-3 yarn graph:impact \ src/redux/slices/salon/calendar/index.js # Nhiều seed cùng lúc yarn graph:impact \ src/redux/slices/salon/calendar/*.js
# Mở SVG viz trong browser
yarn graph:viz \
src/shared/providers/ModalProvider.js
Highlight hub files (đỏ) và leaf files (xanh).
| Loại task | Workflow | Blast radius limit |
|---|---|---|
| Bug fix | graph:impact file gốc → nếu >50, cân nhắc patch local thay vì shared edit |
<50 |
| Small change | graph:impact trước khi viết code, đọc top 10 file affected |
<100 |
| New feature | graph:impact các seed dự kiến touch, viết ADR ngắn nếu >200 |
<200 |
| Refactor | Slice plan kiểu Hop 1/2/3 dùng graph:impact per hop |
tracked |
| Upgrade lib | graph:impact import path → biết phạm vi changelog cần đọc |
tracked |
| Performance | graph:viz để hiểu render tree, không chỉ profile |
− |
| Large refactor | Plan đầy đủ với gate per hop. Re-run graph:impact sau mỗi hop để verify blast không tăng |
staged |
Slice cụ thể để refactor Calendar theo Keep-Stack Phase 2. Đã chạy graph:impact để pre-fill blast radius cho mỗi hop. Mỗi hop có gate riêng — không qua gate là không qua hop tiếp.
shared/utils/calendartách thànhcalendar/projection/- Move
Helpers.jscalc functions ra projection module - Inject vào slice qua adapter, không thay slice contract
- Unit test projection independently
Projection engine có 80%+ test coverage. Calendar slice không có behavior change.
- Thêm event queue giữa
useHandleSignalCalendarvà slice - Coalesce burst (5+ events trong 100ms) thành 1 batch
- Defer layout invalidation qua
InteractionManager - Benchmark trên D3 với realtime burst fixture
Realtime burst (20 events/s) không drop FPS dưới 45 trên D3.
- Tách selection state ra
calendarSelectionslice - Replace
JSON.parse(JSON.stringify(...))bằng immer/structuredClone - Stable selector references (reselect + memoize-one)
- Replace
JSON.stringifymemoization bằng deep-equal stable hooks
Selectors không rebuild khi unrelated slice action dispatch. Render churn giảm >50% trên D3.
Foundation ↓ Calendar ↓ Calendar/Checkout handoff ↓ Payment ↓ Receipt/Printer ↓ Pilot
Đường ngắn nhất từ "có codebase" đến "có thể bán cho merchant". Mỗi node phụ thuộc node trước. Tăng tốc Calendar không giúp gì nếu Payment chưa pass D3 hardware test. Refactor Inventory không nên đặt trên critical path.