@atlashub/smartstack-cli 3.39.0 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.documentation/apex.html +644 -644
  2. package/.documentation/css/styles.css +2320 -2320
  3. package/.documentation/init.html +1377 -1377
  4. package/.documentation/js/app.js +780 -780
  5. package/.documentation/prd-json-v2.0.0.md +396 -396
  6. package/.documentation/testing-ba-e2e.md +462 -462
  7. package/config/default-config.json +95 -95
  8. package/config/mcp-defaults.json +62 -62
  9. package/config/settings.json +53 -53
  10. package/config/settings.local.example.json +16 -16
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp-entry.mjs +6 -4
  13. package/dist/mcp-entry.mjs.map +1 -1
  14. package/package.json +115 -115
  15. package/scripts/extract-api-endpoints.ts +325 -325
  16. package/scripts/extract-business-rules.ts +440 -440
  17. package/scripts/generate-doc-with-mock-ui.ts +804 -804
  18. package/scripts/health-check.sh +168 -168
  19. package/scripts/postinstall.js +18 -18
  20. package/templates/agents/action.md +37 -37
  21. package/templates/agents/ba-reader.md +378 -378
  22. package/templates/agents/ba-writer.md +861 -861
  23. package/templates/agents/code-reviewer.md +163 -163
  24. package/templates/agents/db-reader.md +149 -149
  25. package/templates/agents/docs-context-reader.md +143 -143
  26. package/templates/agents/docs-sync-checker.md +122 -122
  27. package/templates/agents/efcore/conflicts.md +84 -84
  28. package/templates/agents/efcore/db-deploy.md +74 -74
  29. package/templates/agents/efcore/db-reset.md +85 -85
  30. package/templates/agents/efcore/db-seed.md +61 -61
  31. package/templates/agents/efcore/db-status.md +86 -86
  32. package/templates/agents/efcore/migration.md +186 -186
  33. package/templates/agents/efcore/rebase-snapshot.md +108 -108
  34. package/templates/agents/efcore/scan.md +92 -92
  35. package/templates/agents/efcore/squash.md +161 -161
  36. package/templates/agents/explore-codebase.md +66 -66
  37. package/templates/agents/explore-docs.md +98 -98
  38. package/templates/agents/fix-grammar.md +50 -50
  39. package/templates/agents/gitflow/abort.md +45 -45
  40. package/templates/agents/gitflow/cleanup.md +96 -96
  41. package/templates/agents/gitflow/commit.md +236 -236
  42. package/templates/agents/gitflow/exec.md +48 -48
  43. package/templates/agents/gitflow/finish.md +146 -146
  44. package/templates/agents/gitflow/init-clone.md +199 -199
  45. package/templates/agents/gitflow/init-detect.md +137 -137
  46. package/templates/agents/gitflow/init-validate.md +225 -225
  47. package/templates/agents/gitflow/init.md +340 -340
  48. package/templates/agents/gitflow/merge.md +145 -145
  49. package/templates/agents/gitflow/plan.md +42 -42
  50. package/templates/agents/gitflow/pr.md +191 -191
  51. package/templates/agents/gitflow/review.md +49 -49
  52. package/templates/agents/gitflow/start.md +147 -147
  53. package/templates/agents/gitflow/status.md +95 -95
  54. package/templates/agents/mcp-healthcheck.md +163 -163
  55. package/templates/agents/snipper.md +37 -37
  56. package/templates/agents/websearch.md +46 -46
  57. package/templates/hooks/appsettings-guard.sh +76 -76
  58. package/templates/hooks/docs-drift-check.md +96 -96
  59. package/templates/hooks/ef-migration-check.md +139 -139
  60. package/templates/hooks/hooks.json +58 -58
  61. package/templates/hooks/mcp-check.md +64 -64
  62. package/templates/hooks/ralph-mcp-logger.sh +46 -46
  63. package/templates/hooks/ralph-session-end.sh +69 -69
  64. package/templates/hooks/stop-hook.sh +177 -177
  65. package/templates/hooks/wsl-dotnet-cleanup.sh +24 -24
  66. package/templates/mcp-scaffolding/component.tsx.hbs +318 -318
  67. package/templates/mcp-scaffolding/controller.cs.hbs +192 -192
  68. package/templates/mcp-scaffolding/entity-extension.cs.hbs +239 -239
  69. package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -116
  70. package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -133
  71. package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -126
  72. package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -261
  73. package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -53
  74. package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +436 -436
  75. package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -239
  76. package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -441
  77. package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -442
  78. package/templates/mcp-scaffolding/tests/service.test.cs.hbs +402 -402
  79. package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -428
  80. package/templates/project/DependencyInjection.Application.cs.template +25 -25
  81. package/templates/project/DependencyInjection.Infrastructure.cs.template +61 -61
  82. package/templates/project/DesignTimeExtensionsDbContextFactory.cs.template +70 -70
  83. package/templates/project/ExampleEntity.cs.template +116 -116
  84. package/templates/project/ExampleEntityConfiguration.cs.template +64 -64
  85. package/templates/project/ExampleService.cs.template +146 -146
  86. package/templates/project/ExtensionsDbContext.cs.template +41 -41
  87. package/templates/project/IExtensionsDbContext.cs.template +22 -22
  88. package/templates/project/Program.cs.template +47 -47
  89. package/templates/project/README.md +79 -79
  90. package/templates/project/api.ts.template +12 -12
  91. package/templates/project/appsettings.json.template +170 -170
  92. package/templates/project/claude-settings.json.template +5 -5
  93. package/templates/project/test-frontend/msw/handlers.ts +58 -58
  94. package/templates/project/test-frontend/msw/server.ts +25 -25
  95. package/templates/project/test-frontend/setup.ts +16 -16
  96. package/templates/project/test-frontend/test-utils.tsx +59 -59
  97. package/templates/project/test-frontend/vitest.config.ts +31 -31
  98. package/templates/ralph/README.md +93 -93
  99. package/templates/ralph/ralph.config.yaml +113 -113
  100. package/templates/scripts/setup-ralph-loop.sh +173 -173
  101. package/templates/skills/_resources/config-safety.md +61 -61
  102. package/templates/skills/_resources/context-digest-template.md +53 -53
  103. package/templates/skills/_resources/doc-context-cache.md +60 -60
  104. package/templates/skills/_resources/docs-manifest-schema.md +155 -155
  105. package/templates/skills/_resources/formatting-guide.md +124 -124
  106. package/templates/skills/_resources/mcp-validate-documentation-spec.md +181 -181
  107. package/templates/skills/_shared.md +228 -228
  108. package/templates/skills/admin/SKILL.md +48 -48
  109. package/templates/skills/ai-prompt/SKILL.md +107 -107
  110. package/templates/skills/ai-prompt/steps/step-00-init.md +47 -47
  111. package/templates/skills/ai-prompt/steps/step-01-implementation.md +122 -122
  112. package/templates/skills/apex/SKILL.md +168 -168
  113. package/templates/skills/apex/_shared.md +141 -141
  114. package/templates/skills/apex/references/agent-teams-protocol.md +164 -164
  115. package/templates/skills/apex/references/analysis-methods.md +141 -141
  116. package/templates/skills/apex/references/challenge-questions.md +145 -145
  117. package/templates/skills/apex/references/code-generation.md +412 -412
  118. package/templates/skills/apex/references/core-seed-data.md +1437 -1437
  119. package/templates/skills/apex/references/error-classification.md +144 -144
  120. package/templates/skills/apex/references/examine-build-validation.md +82 -82
  121. package/templates/skills/apex/references/execution-frontend-gates.md +177 -177
  122. package/templates/skills/apex/references/execution-frontend-patterns.md +105 -105
  123. package/templates/skills/apex/references/execution-layer1-rules.md +96 -96
  124. package/templates/skills/apex/references/initialization-challenge-flow.md +110 -110
  125. package/templates/skills/apex/references/planning-layer-mapping.md +151 -151
  126. package/templates/skills/apex/references/post-checks.md +1584 -1584
  127. package/templates/skills/apex/references/smartstack-api.md +1053 -1053
  128. package/templates/skills/apex/references/smartstack-frontend.md +1571 -1571
  129. package/templates/skills/apex/references/smartstack-layers.md +402 -402
  130. package/templates/skills/apex/steps/step-00-init.md +307 -307
  131. package/templates/skills/apex/steps/step-01-analyze.md +165 -165
  132. package/templates/skills/apex/steps/step-02-plan.md +144 -144
  133. package/templates/skills/apex/steps/step-03-execute.md +328 -328
  134. package/templates/skills/apex/steps/step-04-examine.md +263 -263
  135. package/templates/skills/apex/steps/step-05-deep-review.md +129 -129
  136. package/templates/skills/apex/steps/step-06-resolve.md +101 -101
  137. package/templates/skills/apex/steps/step-07-tests.md +238 -238
  138. package/templates/skills/apex/steps/step-08-run-tests.md +125 -125
  139. package/templates/skills/application/SKILL.md +4 -4
  140. package/templates/skills/application/references/application-roles-template.md +227 -227
  141. package/templates/skills/application/references/backend-controller-hierarchy.md +58 -58
  142. package/templates/skills/application/references/backend-entity-seeding.md +72 -72
  143. package/templates/skills/application/references/backend-seeding-and-dto-output.md +83 -83
  144. package/templates/skills/application/references/backend-table-prefix-mapping.md +79 -79
  145. package/templates/skills/application/references/backend-verification.md +88 -88
  146. package/templates/skills/application/references/frontend-i18n-and-output.md +67 -67
  147. package/templates/skills/application/references/frontend-route-naming.md +117 -117
  148. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +107 -107
  149. package/templates/skills/application/references/frontend-verification.md +156 -156
  150. package/templates/skills/application/references/migration-checklist-troubleshooting.md +1 -1
  151. package/templates/skills/application/references/provider-template.md +177 -177
  152. package/templates/skills/application/references/roles-client-project-handling.md +55 -55
  153. package/templates/skills/application/references/roles-fallback-procedure.md +149 -149
  154. package/templates/skills/application/references/test-coverage-requirements.md +213 -213
  155. package/templates/skills/application/references/test-frontend.md +73 -73
  156. package/templates/skills/application/references/test-prerequisites.md +72 -72
  157. package/templates/skills/application/steps/step-05-frontend.md +176 -176
  158. package/templates/skills/application/steps/step-06-migration.md +193 -193
  159. package/templates/skills/application/steps/step-07-tests.md +356 -356
  160. package/templates/skills/application/steps/step-08-documentation.md +137 -137
  161. package/templates/skills/application/templates-backend.md +463 -463
  162. package/templates/skills/application/templates-frontend.md +685 -685
  163. package/templates/skills/application/templates-i18n.md +520 -520
  164. package/templates/skills/application/templates-seed.md +1096 -1096
  165. package/templates/skills/business-analyse/SKILL.md +327 -327
  166. package/templates/skills/business-analyse/_architecture.md +123 -123
  167. package/templates/skills/business-analyse/_elicitation.md +206 -206
  168. package/templates/skills/business-analyse/_module-loop.md +115 -115
  169. package/templates/skills/business-analyse/_shared.md +383 -383
  170. package/templates/skills/business-analyse/_suggestions.md +34 -34
  171. package/templates/skills/business-analyse/html/ba-interactive.html +4477 -4477
  172. package/templates/skills/business-analyse/html/build-html.js +77 -77
  173. package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +150 -150
  174. package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +227 -227
  175. package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +199 -199
  176. package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +205 -205
  177. package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +647 -647
  178. package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +195 -195
  179. package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +92 -92
  180. package/templates/skills/business-analyse/html/src/scripts/08-editing.js +135 -135
  181. package/templates/skills/business-analyse/html/src/scripts/09-export.js +168 -168
  182. package/templates/skills/business-analyse/html/src/scripts/10-comments.js +171 -171
  183. package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +166 -166
  184. package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -38
  185. package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -101
  186. package/templates/skills/business-analyse/html/src/styles/03-navigation.css +120 -120
  187. package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -196
  188. package/templates/skills/business-analyse/html/src/styles/05-modules.css +454 -454
  189. package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +272 -272
  190. package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -184
  191. package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +241 -241
  192. package/templates/skills/business-analyse/html/src/template.html +516 -516
  193. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +546 -546
  194. package/templates/skills/business-analyse/questionnaire/00-application.md +160 -160
  195. package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -85
  196. package/templates/skills/business-analyse/questionnaire/01-context.md +185 -185
  197. package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +189 -189
  198. package/templates/skills/business-analyse/questionnaire/03-scope.md +164 -164
  199. package/templates/skills/business-analyse/questionnaire/04-data.md +88 -88
  200. package/templates/skills/business-analyse/questionnaire/05-integrations.md +58 -58
  201. package/templates/skills/business-analyse/questionnaire/06-security.md +68 -68
  202. package/templates/skills/business-analyse/questionnaire/07-ui.md +76 -76
  203. package/templates/skills/business-analyse/questionnaire/08-performance.md +42 -42
  204. package/templates/skills/business-analyse/questionnaire/09-constraints.md +45 -45
  205. package/templates/skills/business-analyse/questionnaire/10-documentation.md +43 -43
  206. package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +59 -59
  207. package/templates/skills/business-analyse/questionnaire/12-migration.md +58 -58
  208. package/templates/skills/business-analyse/questionnaire/13-cross-module.md +69 -69
  209. package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -135
  210. package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -136
  211. package/templates/skills/business-analyse/questionnaire.md +337 -337
  212. package/templates/skills/business-analyse/react/application-viewer.md +242 -242
  213. package/templates/skills/business-analyse/react/components.md +551 -551
  214. package/templates/skills/business-analyse/react/i18n-template.md +306 -306
  215. package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -169
  216. package/templates/skills/business-analyse/references/agent-module-prompt.md +362 -362
  217. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +557 -557
  218. package/templates/skills/business-analyse/references/analysis-semantic-checks.md +190 -190
  219. package/templates/skills/business-analyse/references/cache-warming-strategy.md +566 -566
  220. package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +41 -41
  221. package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +74 -74
  222. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +115 -115
  223. package/templates/skills/business-analyse/references/cadrage-shared-modules.md +68 -69
  224. package/templates/skills/business-analyse/references/cadrage-structure-cards.md +85 -85
  225. package/templates/skills/business-analyse/references/compilation-structure-cards.md +297 -297
  226. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +107 -107
  227. package/templates/skills/business-analyse/references/deploy-data-build.md +180 -180
  228. package/templates/skills/business-analyse/references/deploy-modes.md +118 -118
  229. package/templates/skills/business-analyse/references/detection-strategies.md +424 -424
  230. package/templates/skills/business-analyse/references/entity-architecture-decision.md +218 -218
  231. package/templates/skills/business-analyse/references/handoff-file-templates.md +120 -120
  232. package/templates/skills/business-analyse/references/handoff-mappings.md +81 -81
  233. package/templates/skills/business-analyse/references/handoff-seeddata-generation.md +312 -312
  234. package/templates/skills/business-analyse/references/html-data-mapping.md +299 -299
  235. package/templates/skills/business-analyse/references/init-schema-deployment.md +65 -65
  236. package/templates/skills/business-analyse/references/naming-conventions.md +243 -243
  237. package/templates/skills/business-analyse/references/prd-generation.md +258 -258
  238. package/templates/skills/business-analyse/references/review-data-mapping.md +363 -363
  239. package/templates/skills/business-analyse/references/robustness-checks.md +542 -542
  240. package/templates/skills/business-analyse/references/spec-auto-inference.md +111 -111
  241. package/templates/skills/business-analyse/references/team-orchestration.md +1022 -1022
  242. package/templates/skills/business-analyse/references/ui-dashboard-spec.md +85 -85
  243. package/templates/skills/business-analyse/references/ui-resource-cards.md +259 -259
  244. package/templates/skills/business-analyse/references/validate-incremental-html.md +121 -121
  245. package/templates/skills/business-analyse/references/validation-checklist.md +347 -347
  246. package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -335
  247. package/templates/skills/business-analyse/schemas/application-schema.json +453 -453
  248. package/templates/skills/business-analyse/schemas/feature-schema.json +53 -53
  249. package/templates/skills/business-analyse/schemas/project-schema.json +485 -485
  250. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +201 -201
  251. package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +82 -82
  252. package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +80 -80
  253. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +70 -70
  254. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +547 -547
  255. package/templates/skills/business-analyse/schemas/sections/validation-schema.json +93 -93
  256. package/templates/skills/business-analyse/schemas/shared/common-defs.json +226 -226
  257. package/templates/skills/business-analyse/steps/step-00-init.md +575 -576
  258. package/templates/skills/business-analyse/steps/step-01-cadrage.md +767 -767
  259. package/templates/skills/business-analyse/steps/step-01b-applications.md +419 -419
  260. package/templates/skills/business-analyse/steps/step-02-decomposition.md +387 -387
  261. package/templates/skills/business-analyse/steps/step-03a-data.md +16 -16
  262. package/templates/skills/business-analyse/steps/step-03a1-setup.md +506 -506
  263. package/templates/skills/business-analyse/steps/step-03a2-analysis.md +252 -252
  264. package/templates/skills/business-analyse/steps/step-03b-ui.md +425 -425
  265. package/templates/skills/business-analyse/steps/step-03c-compile.md +611 -611
  266. package/templates/skills/business-analyse/steps/step-03d-validate.md +783 -783
  267. package/templates/skills/business-analyse/steps/step-04-consolidation.md +17 -17
  268. package/templates/skills/business-analyse/steps/step-04a-collect.md +415 -415
  269. package/templates/skills/business-analyse/steps/step-04b-analyze.md +163 -163
  270. package/templates/skills/business-analyse/steps/step-04c-decide.md +186 -186
  271. package/templates/skills/business-analyse/steps/step-05a-handoff.md +840 -840
  272. package/templates/skills/business-analyse/steps/step-05b-deploy.md +522 -522
  273. package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +703 -703
  274. package/templates/skills/business-analyse/steps/step-06-review.md +278 -278
  275. package/templates/skills/business-analyse/templates/tpl-frd.md +168 -168
  276. package/templates/skills/business-analyse/templates/tpl-handoff.md +186 -186
  277. package/templates/skills/business-analyse/templates/tpl-launch-displays.md +59 -59
  278. package/templates/skills/business-analyse/templates/tpl-progress.md +172 -172
  279. package/templates/skills/business-analyse/templates-frd.md +476 -476
  280. package/templates/skills/business-analyse/templates-react.md +574 -574
  281. package/templates/skills/cc-agent/SKILL.md +129 -129
  282. package/templates/skills/cc-agent/references/agent-behavior-patterns.md +95 -95
  283. package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -213
  284. package/templates/skills/cc-agent/references/permission-modes.md +102 -102
  285. package/templates/skills/cc-agent/references/tools-reference.md +144 -144
  286. package/templates/skills/cc-agent/steps/step-00-init.md +134 -134
  287. package/templates/skills/cc-agent/steps/step-01-design.md +186 -186
  288. package/templates/skills/cc-agent/steps/step-02-generate.md +131 -131
  289. package/templates/skills/cc-agent/steps/step-03-validate.md +130 -130
  290. package/templates/skills/cc-agent/templates/agent-categorized.md +67 -67
  291. package/templates/skills/cc-agent/templates/agent-standalone.md +56 -56
  292. package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -94
  293. package/templates/skills/cc-audit/SKILL.md +108 -108
  294. package/templates/skills/cc-audit/references/agent-checklist.md +91 -91
  295. package/templates/skills/cc-audit/references/hook-checklist.md +110 -110
  296. package/templates/skills/cc-audit/references/skill-checklist.md +70 -70
  297. package/templates/skills/cc-audit/steps/step-00-init.md +98 -98
  298. package/templates/skills/cc-audit/steps/step-01-scan.md +142 -142
  299. package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -158
  300. package/templates/skills/cc-audit/steps/step-03-report.md +142 -142
  301. package/templates/skills/cc-skill/SKILL.md +134 -134
  302. package/templates/skills/cc-skill/references/best-practices.md +167 -167
  303. package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -182
  304. package/templates/skills/cc-skill/references/skill-patterns.md +199 -199
  305. package/templates/skills/cc-skill/steps/step-00-init.md +119 -119
  306. package/templates/skills/cc-skill/steps/step-01-design.md +199 -199
  307. package/templates/skills/cc-skill/steps/step-02-generate.md +145 -145
  308. package/templates/skills/cc-skill/steps/step-03-steps.md +151 -151
  309. package/templates/skills/cc-skill/steps/step-04-validate.md +124 -124
  310. package/templates/skills/cc-skill/templates/skill-forked.md +85 -85
  311. package/templates/skills/cc-skill/templates/skill-progressive.md +102 -102
  312. package/templates/skills/cc-skill/templates/skill-simple.md +75 -75
  313. package/templates/skills/cc-skill/templates/step-template.md +82 -82
  314. package/templates/skills/check-version/SKILL.md +196 -196
  315. package/templates/skills/controller/SKILL.md +162 -162
  316. package/templates/skills/controller/postman-templates.md +614 -614
  317. package/templates/skills/controller/references/controller-code-templates.md +159 -159
  318. package/templates/skills/controller/references/mcp-scaffold-workflow.md +209 -209
  319. package/templates/skills/controller/references/permission-sync-templates.md +149 -149
  320. package/templates/skills/controller/steps/step-00-init.md +193 -191
  321. package/templates/skills/controller/steps/step-01-analyze.md +146 -146
  322. package/templates/skills/controller/steps/step-02-plan.md +176 -176
  323. package/templates/skills/controller/steps/step-03-generate.md +189 -189
  324. package/templates/skills/controller/steps/step-04-perms.md +80 -80
  325. package/templates/skills/controller/steps/step-05-validate.md +107 -107
  326. package/templates/skills/controller/templates.md +1555 -1555
  327. package/templates/skills/debug/SKILL.md +70 -70
  328. package/templates/skills/debug/references/team-protocol.md +232 -232
  329. package/templates/skills/debug/steps/step-00-init.md +57 -57
  330. package/templates/skills/debug/steps/step-01-analyze.md +219 -219
  331. package/templates/skills/debug/steps/step-02-resolve.md +85 -85
  332. package/templates/skills/documentation/SKILL.md +132 -132
  333. package/templates/skills/documentation/data-schema.md +227 -227
  334. package/templates/skills/documentation/steps/step-00-init.md +70 -70
  335. package/templates/skills/documentation/steps/step-01-scan.md +113 -113
  336. package/templates/skills/documentation/steps/step-02-generate.md +231 -231
  337. package/templates/skills/documentation/steps/step-03-validate.md +251 -238
  338. package/templates/skills/documentation/templates.md +662 -663
  339. package/templates/skills/efcore/SKILL.md +167 -167
  340. package/templates/skills/efcore/references/both-contexts.md +32 -32
  341. package/templates/skills/efcore/references/database-operations.md +67 -67
  342. package/templates/skills/efcore/references/destructive-operations.md +38 -38
  343. package/templates/skills/efcore/references/reset-operations.md +81 -81
  344. package/templates/skills/efcore/references/seed-methods.md +86 -86
  345. package/templates/skills/efcore/references/shared-init-functions.md +250 -250
  346. package/templates/skills/efcore/references/sql-objects-injection.md +61 -61
  347. package/templates/skills/efcore/references/troubleshooting.md +81 -81
  348. package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -227
  349. package/templates/skills/efcore/steps/db/step-deploy.md +217 -217
  350. package/templates/skills/efcore/steps/db/step-reset.md +186 -186
  351. package/templates/skills/efcore/steps/db/step-seed.md +166 -166
  352. package/templates/skills/efcore/steps/db/step-status.md +173 -173
  353. package/templates/skills/efcore/steps/migration/step-00-init.md +102 -102
  354. package/templates/skills/efcore/steps/migration/step-01-check.md +164 -164
  355. package/templates/skills/efcore/steps/migration/step-02-create.md +160 -160
  356. package/templates/skills/efcore/steps/migration/step-03-validate.md +168 -168
  357. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +173 -173
  358. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +100 -100
  359. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +115 -115
  360. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +112 -112
  361. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +157 -157
  362. package/templates/skills/efcore/steps/shared/step-00-init.md +131 -131
  363. package/templates/skills/efcore/steps/squash/step-00-init.md +141 -141
  364. package/templates/skills/efcore/steps/squash/step-01-backup.md +120 -120
  365. package/templates/skills/efcore/steps/squash/step-02-fetch.md +168 -168
  366. package/templates/skills/efcore/steps/squash/step-03-create.md +184 -184
  367. package/templates/skills/efcore/steps/squash/step-04-validate.md +174 -174
  368. package/templates/skills/explore/SKILL.md +98 -98
  369. package/templates/skills/feature-full/SKILL.md +111 -111
  370. package/templates/skills/feature-full/steps/step-00-init.md +57 -57
  371. package/templates/skills/feature-full/steps/step-01-implementation.md +120 -120
  372. package/templates/skills/gitflow/SKILL.md +377 -377
  373. package/templates/skills/gitflow/_shared.md +620 -620
  374. package/templates/skills/gitflow/phases/abort.md +189 -189
  375. package/templates/skills/gitflow/phases/cleanup.md +234 -234
  376. package/templates/skills/gitflow/phases/status.md +192 -192
  377. package/templates/skills/gitflow/references/commit-message-generation.md +58 -58
  378. package/templates/skills/gitflow/references/commit-migration-validation.md +49 -49
  379. package/templates/skills/gitflow/references/finish-cleanup.md +55 -55
  380. package/templates/skills/gitflow/references/finish-version-bumping.md +45 -45
  381. package/templates/skills/gitflow/references/init-config-template.md +135 -135
  382. package/templates/skills/gitflow/references/init-environment-detection.md +41 -41
  383. package/templates/skills/gitflow/references/init-name-normalization.md +103 -103
  384. package/templates/skills/gitflow/references/init-questions.md +185 -185
  385. package/templates/skills/gitflow/references/init-structure-creation.md +75 -75
  386. package/templates/skills/gitflow/references/init-version-detection.md +21 -21
  387. package/templates/skills/gitflow/references/init-workspace-detection.md +43 -43
  388. package/templates/skills/gitflow/references/merge-ci-status.md +36 -36
  389. package/templates/skills/gitflow/references/merge-execution.md +62 -62
  390. package/templates/skills/gitflow/references/merge-pr-context.md +76 -76
  391. package/templates/skills/gitflow/references/plan-template.md +69 -69
  392. package/templates/skills/gitflow/references/pr-build-checks.md +60 -60
  393. package/templates/skills/gitflow/references/pr-generation.md +58 -58
  394. package/templates/skills/gitflow/references/start-branch-normalization.md +28 -28
  395. package/templates/skills/gitflow/references/start-efcore-preflight.md +70 -70
  396. package/templates/skills/gitflow/references/start-local-config.md +113 -113
  397. package/templates/skills/gitflow/references/start-worktree-creation.md +50 -50
  398. package/templates/skills/gitflow/references/sync-push-verify.md +44 -44
  399. package/templates/skills/gitflow/references/sync-rebase-conflicts.md +38 -38
  400. package/templates/skills/gitflow/steps/step-commit.md +199 -199
  401. package/templates/skills/gitflow/steps/step-finish.md +147 -147
  402. package/templates/skills/gitflow/steps/step-init.md +190 -190
  403. package/templates/skills/gitflow/steps/step-merge.md +85 -85
  404. package/templates/skills/gitflow/steps/step-plan.md +151 -151
  405. package/templates/skills/gitflow/steps/step-pr.md +199 -199
  406. package/templates/skills/gitflow/steps/step-start.md +195 -195
  407. package/templates/skills/gitflow/steps/step-sync.md +161 -161
  408. package/templates/skills/gitflow/templates/config.json +72 -72
  409. package/templates/skills/mcp/SKILL.md +62 -62
  410. package/templates/skills/mcp/steps/step-01-healthcheck.md +108 -108
  411. package/templates/skills/mcp/steps/step-02-tools.md +73 -73
  412. package/templates/skills/notification/SKILL.md +173 -173
  413. package/templates/skills/quick-search/SKILL.md +99 -99
  414. package/templates/skills/ralph-loop/SKILL.md +234 -234
  415. package/templates/skills/ralph-loop/references/category-completeness.md +185 -185
  416. package/templates/skills/ralph-loop/references/category-rules.md +96 -96
  417. package/templates/skills/ralph-loop/references/compact-loop.md +300 -300
  418. package/templates/skills/ralph-loop/references/init-resume-recovery.md +127 -127
  419. package/templates/skills/ralph-loop/references/module-transition.md +151 -151
  420. package/templates/skills/ralph-loop/references/multi-module-queue.md +171 -171
  421. package/templates/skills/ralph-loop/references/parallel-execution.md +246 -246
  422. package/templates/skills/ralph-loop/references/section-splitting.md +439 -439
  423. package/templates/skills/ralph-loop/references/task-transform-legacy.md +256 -256
  424. package/templates/skills/ralph-loop/references/team-orchestration.md +547 -547
  425. package/templates/skills/ralph-loop/steps/step-00-init.md +150 -150
  426. package/templates/skills/ralph-loop/steps/step-01-task.md +174 -174
  427. package/templates/skills/ralph-loop/steps/step-02-execute.md +177 -177
  428. package/templates/skills/ralph-loop/steps/step-03-commit.md +92 -92
  429. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -207
  430. package/templates/skills/ralph-loop/steps/step-05-report.md +175 -175
  431. package/templates/skills/refactor/SKILL.md +56 -56
  432. package/templates/skills/refactor/steps/step-01-discover.md +60 -60
  433. package/templates/skills/refactor/steps/step-02-execute.md +67 -67
  434. package/templates/skills/review-code/SKILL.md +94 -94
  435. package/templates/skills/review-code/references/clean-code-principles.md +292 -292
  436. package/templates/skills/review-code/references/code-quality-metrics.md +174 -174
  437. package/templates/skills/review-code/references/feedback-patterns.md +149 -149
  438. package/templates/skills/review-code/references/owasp-api-top10.md +243 -243
  439. package/templates/skills/review-code/references/security-checklist.md +212 -212
  440. package/templates/skills/review-code/steps/step-01-smartstack.md +96 -96
  441. package/templates/skills/review-code/steps/step-02-detailed-review.md +80 -80
  442. package/templates/skills/review-code/steps/step-03-react.md +44 -44
  443. package/templates/skills/ui-components/SKILL.md +137 -137
  444. package/templates/skills/ui-components/accessibility.md +170 -170
  445. package/templates/skills/ui-components/patterns/dashboard-chart.md +327 -327
  446. package/templates/skills/ui-components/patterns/data-table.md +39 -39
  447. package/templates/skills/ui-components/patterns/entity-card.md +77 -77
  448. package/templates/skills/ui-components/patterns/grid-layout.md +91 -91
  449. package/templates/skills/ui-components/patterns/kanban.md +43 -43
  450. package/templates/skills/ui-components/responsive-guidelines.md +278 -278
  451. package/templates/skills/ui-components/style-guide.md +113 -113
  452. package/templates/skills/utils/SKILL.md +44 -44
  453. package/templates/skills/utils/subcommands/test-web-config.md +152 -152
  454. package/templates/skills/utils/subcommands/test-web.md +123 -123
  455. package/templates/skills/validate/SKILL.md +181 -181
  456. package/templates/skills/validate-feature/SKILL.md +101 -101
  457. package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
  458. package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
  459. package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
  460. package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
  461. package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
  462. package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
  463. package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
  464. package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
  465. package/templates/skills/workflow/SKILL.md +127 -127
  466. package/templates/skills/workflow/steps/step-00-init.md +57 -57
  467. package/templates/skills/workflow/steps/step-01-implementation.md +84 -84
  468. package/templates/test-web/api-health.json +38 -38
  469. package/templates/test-web/minimal.json +19 -19
  470. package/templates/test-web/npm-package.json +46 -46
  471. package/templates/test-web/seo-check.json +54 -54
@@ -1,1571 +1,1571 @@
1
- # SmartStack Frontend Patterns — Mandatory Reference
2
-
3
- > **Loaded by:** step-03 (execution) and step-04 (validation)
4
- > **Purpose:** Defines mandatory frontend patterns extracted from SmartStack.app.
5
- > **Enforcement:** POST-CHECKs in step-04 verify compliance.
6
-
7
- ---
8
-
9
- ## 1. Lazy Loading (React.lazy + Suspense)
10
-
11
- > **ALL page components MUST be lazy-loaded.** Only critical entry pages (HomePage, LoginPage) may use static imports.
12
-
13
- ### Import Pattern
14
-
15
- ```tsx
16
- // Named exports — use .then() to wrap
17
- const EmployeesPage = lazy(() =>
18
- import('@/pages/HumanResources/Employees/EmployeesPage')
19
- .then(m => ({ default: m.EmployeesPage }))
20
- );
21
-
22
- // Default exports — direct lazy
23
- const DashboardPage = lazy(() => import('@/pages/Platform/Admin/DashboardPage'));
24
- ```
25
-
26
- ### Suspense Wrapper
27
-
28
- ```tsx
29
- import { Suspense } from 'react';
30
- import { PageLoader } from '@/components/ui/PageLoader';
31
-
32
- // Route element wrapping
33
- element: (
34
- <Suspense fallback={<PageLoader />}>
35
- <PermissionGuard permissions={ROUTES['hr.employees'].permissions}>
36
- <EmployeesPage />
37
- </PermissionGuard>
38
- </Suspense>
39
- )
40
- ```
41
-
42
- ### Rules
43
-
44
- - **NEVER** static-import page components in route files
45
- - **ALWAYS** use `<Suspense fallback={<PageLoader />}>` around lazy components
46
- - **ALWAYS** use the `.then(m => ({ default: m.ComponentName }))` pattern for named exports
47
- - The unified AppLayout component is ALSO lazy-loaded
48
-
49
- **FORBIDDEN:**
50
- ```tsx
51
- // WRONG: static import in route file
52
- import { EmployeesPage } from '@/pages/HumanResources/Employees/EmployeesPage';
53
-
54
- // WRONG: no Suspense wrapper
55
- element: <EmployeesPage />
56
-
57
- // WRONG: no fallback
58
- <Suspense><EmployeesPage /></Suspense>
59
- ```
60
-
61
- ### Client App.tsx — Lazy Imports Mandatory
62
-
63
- > **CRITICAL:** In the client `App.tsx` (where `contextRoutes` are defined), ALL page imports MUST use `React.lazy()`.
64
-
65
- **CORRECT — Lazy imports in client App.tsx:**
66
- ```tsx
67
- const ClientsListPage = lazy(() =>
68
- import('@/pages/HumanResources/Clients/ClientsListPage')
69
- .then(m => ({ default: m.ClientsListPage }))
70
- );
71
- ```
72
-
73
- **FORBIDDEN — Static imports in client App.tsx:**
74
- ```tsx
75
- // WRONG: Static import kills code splitting
76
- import { ClientsListPage } from '@/pages/HumanResources/Clients/ClientsListPage';
77
- ```
78
-
79
- > **Note:** The `smartstackRoutes.tsx` from the npm package may use static imports internally — this is acceptable for the package. But client `App.tsx` code MUST always use lazy imports for business pages.
80
-
81
- ---
82
-
83
- ## 2. I18n / Translations (react-i18next)
84
-
85
- > **ALL user-facing text MUST use translations.** 4 languages required: fr, en, it, de.
86
-
87
- ### File Structure
88
-
89
- ```
90
- src/i18n/
91
- ├── config.ts # i18n initialization
92
- ├── locales/
93
- │ ├── fr/
94
- │ │ ├── common.json # Shared keys (actions, errors, validation)
95
- │ │ ├── navigation.json # Menu labels
96
- │ │ └── {module}.json # Module-specific keys
97
- │ ├── en/
98
- │ │ └── {module}.json
99
- │ ├── it/
100
- │ │ └── {module}.json
101
- │ └── de/
102
- │ └── {module}.json
103
- ```
104
-
105
- ### Module JSON Template
106
-
107
- Each new module MUST generate a translation file with this structure:
108
-
109
- ```json
110
- {
111
- "title": "Module display name",
112
- "description": "Module description",
113
- "actions": {
114
- "create": "Create {entity}",
115
- "edit": "Edit {entity}",
116
- "delete": "Delete {entity}",
117
- "save": "Save",
118
- "cancel": "Cancel",
119
- "search": "Search...",
120
- "export": "Export",
121
- "refresh": "Refresh"
122
- },
123
- "labels": {
124
- "name": "Name",
125
- "code": "Code",
126
- "description": "Description",
127
- "status": "Status",
128
- "createdAt": "Created at",
129
- "updatedAt": "Updated at",
130
- "createdBy": "Created by",
131
- "isActive": "Active"
132
- },
133
- "columns": {
134
- "name": "Name",
135
- "code": "Code",
136
- "status": "Status",
137
- "actions": "Actions"
138
- },
139
- "form": {
140
- "name": "Name",
141
- "namePlaceholder": "Enter name...",
142
- "code": "Code",
143
- "codePlaceholder": "Enter code...",
144
- "description": "Description",
145
- "descriptionPlaceholder": "Enter description..."
146
- },
147
- "errors": {
148
- "loadFailed": "Failed to load data",
149
- "saveFailed": "Failed to save",
150
- "deleteFailed": "Failed to delete",
151
- "notFound": "Not found",
152
- "permissionDenied": "Permission denied"
153
- },
154
- "validation": {
155
- "nameRequired": "Name is required",
156
- "codeRequired": "Code is required",
157
- "nameMaxLength": "Name must be less than {{max}} characters"
158
- },
159
- "messages": {
160
- "created": "{entity} created successfully",
161
- "updated": "{entity} updated successfully",
162
- "deleted": "{entity} deleted successfully",
163
- "confirmDelete": "Are you sure you want to delete this {entity}?"
164
- },
165
- "empty": {
166
- "title": "No {entity} found",
167
- "description": "Create your first {entity} to get started"
168
- }
169
- }
170
- ```
171
-
172
- ### Usage in Components
173
-
174
- ```tsx
175
- // Hook — specify namespace(s)
176
- const { t } = useTranslation(['employees']);
177
-
178
- // Simple key with MANDATORY fallback
179
- t('employees:title', 'Employees')
180
-
181
- // Key with interpolation
182
- t('employees:messages.created', '{{entity}} created successfully', { entity: 'Employee' })
183
-
184
- // Namespace prefix syntax
185
- t('employees:actions.create', 'Create employee')
186
- t('common:actions.save', 'Save')
187
- t('common:errors.network', 'Network error')
188
- ```
189
-
190
- ### Namespace Registration (CRITICAL)
191
-
192
- > **After creating i18n JSON files, you MUST register each namespace in the i18n config.**
193
- > Root cause (test-apex-007): JSON files existed but namespaces were not registered → `useTranslation(['module'])` returned empty strings.
194
-
195
- In the i18n config file (`src/i18n/config.ts` or `src/i18n/index.ts`), add each new namespace:
196
-
197
- ```typescript
198
- // Example: registering new module namespaces
199
- import employees from './locales/fr/employees.json';
200
- import projects from './locales/fr/projects.json';
201
- import clients from './locales/fr/clients.json';
202
-
203
- // In resources configuration:
204
- resources: {
205
- fr: { employees, projects, clients, common, navigation },
206
- en: { employees: employeesEn, projects: projectsEn, clients: clientsEn, ... },
207
- // ... it, de
208
- }
209
-
210
- // OR with ns array:
211
- ns: ['common', 'navigation', 'employees', 'projects', 'clients'],
212
- ```
213
-
214
- POST-CHECK 45 validates this. Unregistered namespaces → BLOCKING.
215
-
216
- ### Rules
217
-
218
- - **ALWAYS** provide a fallback value as 2nd argument to `t()`
219
- - **ALWAYS** use namespace prefix: `t('namespace:key')`
220
- - **ALWAYS** generate 4 language files (fr, en, it, de) with identical key structures
221
- - **ALWAYS** register new namespaces in i18n config file after creating JSON files
222
- - **NEVER** hardcode user-facing strings in JSX
223
- - **NEVER** use `t('key')` without namespace prefix
224
-
225
- **FORBIDDEN:**
226
- ```tsx
227
- // WRONG: no fallback
228
- t('employees:title')
229
-
230
- // WRONG: no namespace
231
- t('title')
232
-
233
- // WRONG: hardcoded text
234
- <h1>Employees</h1>
235
-
236
- // WRONG: only 2 languages generated
237
- // Must have fr, en, it, de
238
- ```
239
-
240
- ---
241
-
242
- ## 3. Page Structure Pattern
243
-
244
- > **ALL pages MUST follow this structure.** Extracted from SmartStack.app reference implementation.
245
-
246
- ### Standard List Page Template
247
-
248
- ```tsx
249
- import { useState, useCallback, useEffect } from 'react';
250
- import { useTranslation } from 'react-i18next';
251
- import { useNavigate, useParams } from 'react-router-dom';
252
- import { Loader2 } from 'lucide-react';
253
- import { DocToggleButton } from '@/components/docs/DocToggleButton';
254
-
255
- // API hook (generated by scaffold_api_client)
256
- import { useEntityList } from '@/hooks/useEntity';
257
-
258
- export function EntityListPage() {
259
- // 1. HOOKS — always at the top
260
- const { t } = useTranslation(['{module}']);
261
- const navigate = useNavigate();
262
-
263
- // 2. STATE
264
- const [loading, setLoading] = useState(true);
265
- const [error, setError] = useState<string | null>(null);
266
- const [data, setData] = useState<Entity[]>([]);
267
-
268
- // 3. DATA LOADING (useCallback + useEffect)
269
- const loadData = useCallback(async () => {
270
- try {
271
- setLoading(true);
272
- setError(null);
273
- const result = await entityApi.getAll();
274
- setData(result.items);
275
- } catch (err: any) {
276
- setError(err.message || t('{module}:errors.loadFailed', 'Failed to load data'));
277
- } finally {
278
- setLoading(false);
279
- }
280
- }, [t]);
281
-
282
- useEffect(() => {
283
- loadData();
284
- }, [loadData]);
285
-
286
- // 4. LOADING STATE
287
- if (loading) {
288
- return (
289
- <div className="flex items-center justify-center min-h-[400px]">
290
- <Loader2 className="w-8 h-8 animate-spin text-[var(--color-accent-500)]" />
291
- </div>
292
- );
293
- }
294
-
295
- // 5. ERROR STATE
296
- if (error) {
297
- return (
298
- <div className="flex items-center justify-center min-h-[400px]">
299
- <div className="text-center">
300
- <p className="text-[var(--text-secondary)]">{error}</p>
301
- <button
302
- onClick={loadData}
303
- className="mt-4 px-4 py-2 bg-[var(--color-accent-500)] text-white rounded"
304
- >
305
- {t('common:actions.retry', 'Retry')}
306
- </button>
307
- </div>
308
- </div>
309
- );
310
- }
311
-
312
- // 6. CONTENT — create button navigates to /create route
313
- return (
314
- <div className="space-y-6">
315
- {/* Header with DocToggleButton */}
316
- <div className="flex items-center justify-between">
317
- <h1 className="text-2xl font-bold text-[var(--text-primary)]">
318
- {t('{module}:title', 'Module Title')}
319
- </h1>
320
- <div className="flex items-center gap-2">
321
- <DocToggleButton />
322
- <button
323
- onClick={() => navigate('create')}
324
- className="px-4 py-2 bg-[var(--color-accent-500)] text-white rounded"
325
- >
326
- {t('{module}:actions.create', 'Create')}
327
- </button>
328
- </div>
329
- </div>
330
-
331
- {/* Content: SmartTable with row click → detail, edit action → /:id/edit */}
332
- </div>
333
- );
334
- }
335
- ```
336
-
337
- ### Detail Page Pattern
338
-
339
- ```tsx
340
- export function EntityDetailPage() {
341
- const { entityId } = useParams<{ entityId: string }>();
342
- const { t } = useTranslation(['{module}']);
343
- const navigate = useNavigate();
344
-
345
- const [entity, setEntity] = useState<Entity | null>(null);
346
- const [loading, setLoading] = useState(true);
347
- const [activeTab, setActiveTab] = useState('info');
348
-
349
- // Lazy tab loading — load data only when tab is first visited
350
- const visitedTabsRef = useRef<Set<string>>(new Set(['info']));
351
-
352
- useEffect(() => {
353
- if (!visitedTabsRef.current.has(activeTab)) {
354
- visitedTabsRef.current.add(activeTab);
355
- // Load tab-specific data here (e.g., fetch leaves for this employee)
356
- }
357
- }, [activeTab]);
358
-
359
- // Edit button navigates to /:id/edit route (NEVER opens a modal)
360
- const handleEdit = () => navigate(`edit`);
361
-
362
- // ... loading/error/content pattern
363
- }
364
- ```
365
-
366
- ### Tab Behavior Rules (CRITICAL)
367
-
368
- > **CRITICAL: Tabs on detail pages switch content LOCALLY — they NEVER navigate to other pages.**
369
- > Each tab renders its content INLINE within the same page component.
370
- > Sub-resource data (e.g., an employee's leaves) loads via API call filtered by the parent entity ID.
371
-
372
- **Tab state management:**
373
- - Tabs use `useState<TabKey>('info')` for the active tab — LOCAL React state only
374
- - Tab click handler: `onClick={() => setActiveTab(tabKey)}` — NEVER `navigate()`
375
- - Tab content: conditional rendering `{activeTab === 'tabKey' && <TabContent />}`
376
- - Lazy loading: `visitedTabsRef` tracks which tabs have been visited to avoid redundant API calls
377
-
378
- **Tab content for sub-resources:**
379
- ```tsx
380
- // CORRECT — sub-resource data loaded INLINE within the tab
381
- {activeTab === 'leaves' && (
382
- <div>
383
- <LeaveRequestsTable employeeId={entity.id} />
384
- {/* Optional "View all" link INSIDE the tab content area */}
385
- <Link to={`../leaves?employee=${entity.id}`}>
386
- {t('employees:tabs.viewAllLeaves', 'View all leave requests')}
387
- </Link>
388
- </div>
389
- )}
390
- ```
391
-
392
- **FORBIDDEN tab patterns:**
393
- ```tsx
394
- // FORBIDDEN — tab click handler navigates to another page
395
- const handleTabClick = (tab: TabKey) => {
396
- setActiveTab(tab);
397
- if (tab === 'leaves') navigate(`../leaves?employee=${id}`); // ← BREAKS tab UX
398
- };
399
-
400
- // FORBIDDEN — tab content is empty because navigation already left the page
401
- {activeTab === 'info' && <div>...</div>}
402
- // Leaves tab: nothing renders here, user is already on another page
403
- ```
404
-
405
- **Why this matters:**
406
- - Navigating away loses the detail page context (entity data, scroll position, other tab state)
407
- - Users expect tabs to switch content in-place, not redirect to a different page
408
- - The browser back button should go to the list page, not toggle between tabs
409
-
410
- **POST-CHECK 43 enforces this rule.**
411
-
412
- ---
413
-
414
- ## 3b. Form Pages Pattern (Create / Edit)
415
-
416
- > **CRITICAL: ALL forms MUST be full pages with their own URL route.**
417
- > **NEVER use modals, dialogs, drawers, or popups for create/edit forms.**
418
-
419
- ### Route Convention
420
-
421
- > **CRITICAL:** Route paths MUST use **kebab-case** matching the navigation seed data (which uses `ToKebabCase()`).
422
- > - Single word: `employees` (no change needed)
423
- > - Multi-word: `human-resources`, `time-management` (kebab-case with hyphens)
424
- > - **FORBIDDEN:** `humanresources`, `timemanagement` (concatenated words without hyphens)
425
-
426
- | Action | Route pattern | Page component | File location |
427
- |--------|--------------|----------------|---------------|
428
- | Create | `/{module}/create` | `EntityCreatePage` | `src/pages/{App}/{Module}/EntityCreatePage.tsx` |
429
- | Edit | `/{module}/:id/edit` | `EntityEditPage` | `src/pages/{App}/{Module}/EntityEditPage.tsx` |
430
-
431
- ### Create Page Template
432
-
433
- ```tsx
434
- import { useState } from 'react';
435
- import { useTranslation } from 'react-i18next';
436
- import { useNavigate } from 'react-router-dom';
437
-
438
- export function EntityCreatePage() {
439
- const { t } = useTranslation(['{module}']);
440
- const navigate = useNavigate();
441
- const [submitting, setSubmitting] = useState(false);
442
-
443
- const handleSubmit = async (data: CreateEntityDto) => {
444
- try {
445
- setSubmitting(true);
446
- await entityApi.create(data);
447
- navigate(-1); // Back to list
448
- } catch (err: any) {
449
- // Handle validation errors
450
- } finally {
451
- setSubmitting(false);
452
- }
453
- };
454
-
455
- return (
456
- <div className="space-y-6">
457
- {/* Back button */}
458
- <button
459
- onClick={() => navigate(-1)}
460
- className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
461
- >
462
- {t('common:actions.back', 'Back')}
463
- </button>
464
-
465
- {/* Page title */}
466
- <h1 className="text-2xl font-bold text-[var(--text-primary)]">
467
- {t('{module}:actions.create', 'Create {Entity}')}
468
- </h1>
469
-
470
- {/* SmartForm — NEVER in a modal */}
471
- <SmartForm
472
- fields={formFields}
473
- onSubmit={handleSubmit}
474
- onCancel={() => navigate(-1)}
475
- submitting={submitting}
476
- />
477
- </div>
478
- );
479
- }
480
- ```
481
-
482
- ### Edit Page Template
483
-
484
- ```tsx
485
- import { useState, useEffect, useCallback } from 'react';
486
- import { useTranslation } from 'react-i18next';
487
- import { useNavigate, useParams } from 'react-router-dom';
488
- import { Loader2 } from 'lucide-react';
489
-
490
- export function EntityEditPage() {
491
- const { entityId } = useParams<{ entityId: string }>();
492
- const { t } = useTranslation(['{module}']);
493
- const navigate = useNavigate();
494
- const [entity, setEntity] = useState<Entity | null>(null);
495
- const [loading, setLoading] = useState(true);
496
- const [submitting, setSubmitting] = useState(false);
497
-
498
- const loadEntity = useCallback(async () => {
499
- try {
500
- setLoading(true);
501
- const result = await entityApi.getById(entityId!);
502
- setEntity(result);
503
- } catch {
504
- navigate(-1);
505
- } finally {
506
- setLoading(false);
507
- }
508
- }, [entityId, navigate]);
509
-
510
- useEffect(() => { loadEntity(); }, [loadEntity]);
511
-
512
- if (loading) {
513
- return (
514
- <div className="flex items-center justify-center min-h-[400px]">
515
- <Loader2 className="w-8 h-8 animate-spin text-[var(--color-accent-500)]" />
516
- </div>
517
- );
518
- }
519
-
520
- const handleSubmit = async (data: UpdateEntityDto) => {
521
- try {
522
- setSubmitting(true);
523
- await entityApi.update(entityId!, data);
524
- navigate(-1); // Back to detail or list
525
- } catch (err: any) {
526
- // Handle validation errors
527
- } finally {
528
- setSubmitting(false);
529
- }
530
- };
531
-
532
- return (
533
- <div className="space-y-6">
534
- {/* Back button */}
535
- <button
536
- onClick={() => navigate(-1)}
537
- className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
538
- >
539
- {t('common:actions.back', 'Back')}
540
- </button>
541
-
542
- {/* Page title */}
543
- <h1 className="text-2xl font-bold text-[var(--text-primary)]">
544
- {t('{module}:actions.edit', 'Edit {Entity}')}
545
- </h1>
546
-
547
- {/* SmartForm pre-filled — NEVER in a modal */}
548
- <SmartForm
549
- fields={formFields}
550
- initialValues={entity}
551
- onSubmit={handleSubmit}
552
- onCancel={() => navigate(-1)}
553
- submitting={submitting}
554
- />
555
- </div>
556
- );
557
- }
558
- ```
559
-
560
- ### Lazy Loading for Form Pages
561
-
562
- ```tsx
563
- // In route files — form pages are also lazy-loaded
564
- const EntityCreatePage = lazy(() =>
565
- import('@/pages/HumanResources/Employees/EntityCreatePage')
566
- .then(m => ({ default: m.EntityCreatePage }))
567
- );
568
- const EntityEditPage = lazy(() =>
569
- import('@/pages/HumanResources/Employees/EntityEditPage')
570
- .then(m => ({ default: m.EntityEditPage }))
571
- );
572
-
573
- // Route registration — form pages have their own routes
574
- {
575
- path: 'employees',
576
- children: [
577
- { index: true, element: <Suspense fallback={<PageLoader />}><EmployeesPage /></Suspense> },
578
- { path: 'create', element: <Suspense fallback={<PageLoader />}><EntityCreatePage /></Suspense> },
579
- { path: ':id', element: <Suspense fallback={<PageLoader />}><EntityDetailPage /></Suspense> },
580
- { path: ':id/edit', element: <Suspense fallback={<PageLoader />}><EntityEditPage /></Suspense> },
581
- ]
582
- }
583
-
584
- // Section-level routes — children of the module route (when module has sections)
585
- //
586
- // > **IMPORTANT:** The `list` and `detail` sections do NOT generate additional route entries.
587
- // > They are already covered by the module's `index: true` (list) and `path: ':id'` (detail) routes above.
588
- // > Only sections like `dashboard`, `approve`, `import`, etc. generate the section-kebab child routes below.
589
- // > FORBIDDEN: `path: 'list'`, `path: 'detail'` — these would create unreachable duplicate routes.
590
- //
591
- {
592
- path: '{module-kebab}',
593
- children: [
594
- { index: true, element: <Suspense fallback={<PageLoader />}><{Module}Page /></Suspense> },
595
- { path: 'create', element: <Suspense fallback={<PageLoader />}><Create{Module}Page /></Suspense> },
596
- { path: ':id', element: <Suspense fallback={<PageLoader />}><{Module}DetailPage /></Suspense> },
597
- { path: ':id/edit', element: <Suspense fallback={<PageLoader />}><Edit{Module}Page /></Suspense> },
598
- // Section routes as children of module:
599
- // IMPORTANT: "list" and "detail" are NOT separate path segments.
600
- // - "list" section = already handled by the module's index route above (index: true)
601
- // - "detail" section = already handled by the module's :id route above (path: ':id')
602
- // - Only OTHER sections (dashboard, approve, import, etc.) add path segments:
603
- { path: '{section-kebab}', element: <Suspense fallback={<PageLoader />}><{Section}Page /></Suspense> },
604
- { path: '{section-kebab}/create', element: <Suspense fallback={<PageLoader />}><Create{Section}Page /></Suspense> },
605
- { path: '{section-kebab}/:id', element: <Suspense fallback={<PageLoader />}><{Section}DetailPage /></Suspense> },
606
- { path: '{section-kebab}/:id/edit', element: <Suspense fallback={<PageLoader />}><Edit{Section}Page /></Suspense> },
607
- ]
608
- }
609
-
610
- // PermissionGuard for section-level routes
611
- element: (
612
- <Suspense fallback={<PageLoader />}>
613
- <PermissionGuard permissions={ROUTES['app.module.section'].permissions}>
614
- <SectionPage />
615
- </PermissionGuard>
616
- </Suspense>
617
- )
618
- ```
619
-
620
- ### Rules
621
-
622
- - **NEVER** use `<Modal>`, `<Dialog>`, `<Drawer>`, or `<Popup>` for create/edit forms
623
- - **NEVER** use `useState(isOpen)` to toggle form visibility — forms are pages, not overlays
624
- - **ALWAYS** create a dedicated `EntityCreatePage.tsx` and `EntityEditPage.tsx` page component
625
- - **ALWAYS** register create/edit routes alongside list/detail routes
626
- - **ALWAYS** use `navigate('create')` or `navigate(\`${id}/edit\`)` from list/detail pages
627
- - **ALWAYS** include a back button that uses `navigate(-1)` to return to previous page
628
-
629
- **FORBIDDEN:**
630
- ```tsx
631
- // WRONG: modal for create form
632
- const [showCreateModal, setShowCreateModal] = useState(false);
633
- <Modal open={showCreateModal}><CreateForm /></Modal>
634
-
635
- // WRONG: dialog for edit form
636
- <Dialog open={editDialogOpen}><EditForm entity={selected} /></Dialog>
637
-
638
- // WRONG: drawer for form
639
- <Drawer open={isDrawerOpen}><SmartForm /></Drawer>
640
-
641
- // WRONG: inline form toggle
642
- {isEditing ? <EditForm /> : <DetailView />}
643
- ```
644
-
645
- ---
646
-
647
- ## 4. CSS Variables (Theme System)
648
-
649
- > **NEVER use hardcoded Tailwind colors.** ALWAYS use CSS variables for theme support.
650
-
651
- ### Variable Reference
652
-
653
- | Usage | CSS Variable | Example |
654
- |-------|-------------|---------|
655
- | Background | `var(--bg-primary)` | `bg-[var(--bg-primary)]` |
656
- | Card background | `var(--bg-card)` | `bg-[var(--bg-card)]` |
657
- | Text primary | `var(--text-primary)` | `text-[var(--text-primary)]` |
658
- | Text secondary | `var(--text-secondary)` | `text-[var(--text-secondary)]` |
659
- | Borders | `var(--border-color)` | `border-[var(--border-color)]` |
660
- | Accent | `var(--color-accent-500)` | `text-[var(--color-accent-500)]` |
661
- | Card radius | `var(--radius-card)` | `style={{ borderRadius: 'var(--radius-card)' }}` |
662
-
663
- ### Card Pattern
664
-
665
- ```tsx
666
- <div
667
- className="bg-[var(--bg-card)] border border-[var(--border-color)] p-6"
668
- style={{ borderRadius: 'var(--radius-card)' }}
669
- >
670
- <h2 className="text-lg font-semibold text-[var(--text-primary)]">Title</h2>
671
- <p className="text-sm text-[var(--text-secondary)]">Description</p>
672
- </div>
673
- ```
674
-
675
- **FORBIDDEN:**
676
- ```tsx
677
- // WRONG: hardcoded Tailwind colors
678
- className="bg-white border-gray-200 text-gray-900"
679
-
680
- // WRONG: hardcoded hex/rgb
681
- style={{ backgroundColor: '#ffffff', color: '#1a1a1a' }}
682
- ```
683
-
684
- ---
685
-
686
- ## 5. Component Rules
687
-
688
- | Need | Component | Source |
689
- |------|-----------|--------|
690
- | Data table | `SmartTable` | `@/components/SmartTable` |
691
- | Filters | `SmartFilter` | `@/components/SmartFilter` |
692
- | Entity cards | `EntityCard` | `@/components/EntityCard` |
693
- | Forms | `SmartForm` | `@/components/SmartForm` |
694
- | FK field lookup | `EntityLookup` | `@/components/ui/EntityLookup` |
695
- | Statistics | `StatCard` | `@/components/StatCard` |
696
- | Loading spinner | `Loader2` | `lucide-react` |
697
- | Page loader | `PageLoader` | `@/components/ui/PageLoader` |
698
-
699
- ### Rules
700
-
701
- - **NEVER** use raw `<table>` — use SmartTable
702
- - **NEVER** create custom spinners — use `Loader2` from lucide-react
703
- - **NEVER** import axios directly — use `@/services/api/apiClient`
704
- - **ALWAYS** use `PageLoader` as Suspense fallback
705
- - **ALWAYS** use existing shared components before creating new ones
706
-
707
- ---
708
-
709
- ## 6. Foreign Key Fields & Entity Lookup (CRITICAL)
710
-
711
- > **NEVER render a foreign key (Guid) as a plain text input.** FK fields MUST use a searchable lookup component.
712
- > A form asking the user to type a GUID manually is a UX failure. ALL FK fields must provide entity search & selection.
713
-
714
- ### Field Type Classification
715
-
716
- When generating form fields, determine the field type from the entity property:
717
-
718
- | Property type | Form field type | Component |
719
- |---------------|----------------|-----------|
720
- | `string` | Text input | `<input type="text" />` |
721
- | `string?` | Text input (optional) | `<input type="text" />` |
722
- | `Guid` (FK — e.g., `EmployeeId`, `DepartmentId`) | **Entity Lookup** | `<EntityLookup />` |
723
- | `bool` | Toggle/Checkbox | `<input type="checkbox" />` |
724
- | `int` / `decimal` | Number input | `<input type="number" />` |
725
- | `DateTime` | Date picker | `<input type="date" />` |
726
- | `enum` | Select dropdown | `<select>` |
727
-
728
- **How to detect FK fields:** Any property named `{Entity}Id` of type `Guid` that has a corresponding navigation property is a foreign key. Examples: `EmployeeId`, `DepartmentId`, `CategoryId`, `ParentId`.
729
-
730
- ### EntityLookup Component Pattern
731
-
732
- ```tsx
733
- import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
734
- import { useTranslation } from 'react-i18next';
735
- import { Search, X, ChevronDown } from 'lucide-react';
736
- import { apiClient } from '@/services/api/apiClient';
737
-
738
- interface EntityLookupOption {
739
- id: string;
740
- label: string; // Display name (e.g., employee full name)
741
- sublabel?: string; // Secondary info (e.g., department, code)
742
- }
743
-
744
- interface EntityLookupProps {
745
- /** API endpoint to search entities (e.g., '/api/human-resources/employees') */
746
- apiEndpoint: string;
747
- /** Currently selected entity ID */
748
- value: string | null;
749
- /** Callback when entity is selected */
750
- onChange: (id: string | null) => void;
751
- /** Field label */
752
- label: string;
753
- /** Placeholder text */
754
- placeholder?: string;
755
- /** Map API response item to display option */
756
- mapOption: (item: any) => EntityLookupOption;
757
- /** Whether the field is required */
758
- required?: boolean;
759
- /** Whether the field is disabled */
760
- disabled?: boolean;
761
- /** Error message to display */
762
- error?: string;
763
- }
764
-
765
- export function EntityLookup({
766
- apiEndpoint,
767
- value,
768
- onChange,
769
- label,
770
- placeholder,
771
- mapOption,
772
- required = false,
773
- disabled = false,
774
- error,
775
- }: EntityLookupProps) {
776
- const { t } = useTranslation(['common']);
777
- const [search, setSearch] = useState('');
778
- const [options, setOptions] = useState<EntityLookupOption[]>([]);
779
- const [selectedOption, setSelectedOption] = useState<EntityLookupOption | null>(null);
780
- const [isOpen, setIsOpen] = useState(false);
781
- const [loading, setLoading] = useState(false);
782
- const containerRef = useRef<HTMLDivElement>(null);
783
- const debounceRef = useRef<ReturnType<typeof setTimeout>>();
784
-
785
- // Load selected entity display on mount (when value is set but no label)
786
- useEffect(() => {
787
- if (value && !selectedOption) {
788
- apiClient.get(`${apiEndpoint}/${value}`)
789
- .then(res => setSelectedOption(mapOption(res.data)))
790
- .catch(() => { /* Entity not found — clear */ });
791
- }
792
- }, [value, apiEndpoint, mapOption, selectedOption]);
793
-
794
- // Debounced search — 300ms delay, minimum 2 characters
795
- const handleSearch = useCallback((term: string) => {
796
- setSearch(term);
797
- if (debounceRef.current) clearTimeout(debounceRef.current);
798
-
799
- if (term.length < 2) {
800
- setOptions([]);
801
- return;
802
- }
803
-
804
- debounceRef.current = setTimeout(async () => {
805
- setLoading(true);
806
- try {
807
- const res = await apiClient.get(apiEndpoint, {
808
- params: { search: term, pageSize: 20 },
809
- });
810
- setOptions((res.data.items || res.data).map(mapOption));
811
- } catch {
812
- setOptions([]);
813
- } finally {
814
- setLoading(false);
815
- }
816
- }, 300);
817
- }, [apiEndpoint, mapOption]);
818
-
819
- // Load initial options when dropdown opens (show first 20)
820
- const handleOpen = useCallback(async () => {
821
- if (disabled) return;
822
- setIsOpen(true);
823
- if (options.length === 0 && search.length < 2) {
824
- setLoading(true);
825
- try {
826
- const res = await apiClient.get(apiEndpoint, {
827
- params: { pageSize: 20 },
828
- });
829
- setOptions((res.data.items || res.data).map(mapOption));
830
- } catch {
831
- setOptions([]);
832
- } finally {
833
- setLoading(false);
834
- }
835
- }
836
- }, [disabled, apiEndpoint, mapOption, options.length, search.length]);
837
-
838
- // Select entity
839
- const handleSelect = useCallback((option: EntityLookupOption) => {
840
- setSelectedOption(option);
841
- onChange(option.id);
842
- setIsOpen(false);
843
- setSearch('');
844
- }, [onChange]);
845
-
846
- // Clear selection
847
- const handleClear = useCallback(() => {
848
- setSelectedOption(null);
849
- onChange(null);
850
- setSearch('');
851
- }, [onChange]);
852
-
853
- // Close on outside click
854
- useEffect(() => {
855
- const handleClickOutside = (e: MouseEvent) => {
856
- if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
857
- setIsOpen(false);
858
- }
859
- };
860
- document.addEventListener('mousedown', handleClickOutside);
861
- return () => document.removeEventListener('mousedown', handleClickOutside);
862
- }, []);
863
-
864
- return (
865
- <div ref={containerRef} className="relative">
866
- <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
867
- {label} {required && <span className="text-[var(--error-text)]">*</span>}
868
- </label>
869
-
870
- {/* Selected value display OR search input */}
871
- {selectedOption && !isOpen ? (
872
- <div className="flex items-center gap-2 px-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)]">
873
- <div className="flex-1">
874
- <span className="text-[var(--text-primary)]">{selectedOption.label}</span>
875
- {selectedOption.sublabel && (
876
- <span className="ml-2 text-sm text-[var(--text-secondary)]">{selectedOption.sublabel}</span>
877
- )}
878
- </div>
879
- {!disabled && (
880
- <button type="button" onClick={handleClear} className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]">
881
- <X className="w-4 h-4" />
882
- </button>
883
- )}
884
- <button type="button" onClick={handleOpen} className="text-[var(--text-secondary)]">
885
- <ChevronDown className="w-4 h-4" />
886
- </button>
887
- </div>
888
- ) : (
889
- <div className="relative">
890
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
891
- <input
892
- type="text"
893
- value={search}
894
- onChange={(e) => handleSearch(e.target.value)}
895
- onFocus={handleOpen}
896
- placeholder={placeholder || t('common:actions.search', 'Search...')}
897
- disabled={disabled}
898
- className="w-full pl-9 pr-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)] text-[var(--text-primary)] placeholder:text-[var(--text-secondary)] focus:ring-2 focus:ring-[var(--color-accent-500)] focus:border-transparent"
899
- />
900
- </div>
901
- )}
902
-
903
- {/* Dropdown */}
904
- {isOpen && (
905
- <div className="absolute z-50 w-full mt-1 bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[var(--radius-card)] shadow-lg max-h-60 overflow-auto">
906
- {loading ? (
907
- <div className="p-3 text-center text-[var(--text-secondary)]">
908
- {t('common:actions.loading', 'Loading...')}
909
- </div>
910
- ) : options.length === 0 ? (
911
- <div className="p-3 text-center text-[var(--text-secondary)]">
912
- {search.length < 2
913
- ? t('common:actions.typeToSearch', 'Type at least 2 characters to search...')
914
- : t('common:empty.noResults', 'No results found')}
915
- </div>
916
- ) : (
917
- options.map((option) => (
918
- <button
919
- key={option.id}
920
- type="button"
921
- onClick={() => handleSelect(option)}
922
- className="w-full px-3 py-2 text-left hover:bg-[var(--bg-hover)] transition-colors"
923
- >
924
- <div className="text-[var(--text-primary)]">{option.label}</div>
925
- {option.sublabel && (
926
- <div className="text-sm text-[var(--text-secondary)]">{option.sublabel}</div>
927
- )}
928
- </button>
929
- ))
930
- )}
931
- </div>
932
- )}
933
-
934
- {/* Error message */}
935
- {error && (
936
- <p className="mt-1 text-sm text-[var(--error-text)]">{error}</p>
937
- )}
938
- </div>
939
- );
940
- }
941
- ```
942
-
943
- ### Usage in Form Pages
944
-
945
- ```tsx
946
- // In EntityCreatePage.tsx or EntityEditPage.tsx
947
- import { EntityLookup } from '@/components/ui/EntityLookup';
948
-
949
- // Inside the form:
950
- <EntityLookup
951
- apiEndpoint="/api/human-resources/employees"
952
- value={formData.employeeId}
953
- onChange={(id) => handleChange('employeeId', id)}
954
- label={t('module:form.employee', 'Employee')}
955
- placeholder={t('module:form.employeePlaceholder', 'Search for an employee...')}
956
- mapOption={(emp) => ({
957
- id: emp.id,
958
- label: `${emp.firstName} ${emp.lastName}`,
959
- sublabel: emp.department || emp.code,
960
- })}
961
- required
962
- error={errors.employeeId}
963
- />
964
-
965
- // For DepartmentId FK:
966
- <EntityLookup
967
- apiEndpoint="/api/human-resources/departments"
968
- value={formData.departmentId}
969
- onChange={(id) => handleChange('departmentId', id)}
970
- label={t('module:form.department', 'Department')}
971
- placeholder={t('module:form.departmentPlaceholder', 'Search for a department...')}
972
- mapOption={(dept) => ({
973
- id: dept.id,
974
- label: dept.name,
975
- sublabel: dept.code,
976
- })}
977
- required
978
- />
979
- ```
980
-
981
- ### API Search Endpoint Convention (Backend)
982
-
983
- For EntityLookup to work, each entity's API MUST support search via query parameter:
984
-
985
- ```
986
- GET /api/{resource}?search={term}&pageSize=20
987
- ```
988
-
989
- Response format:
990
- ```json
991
- {
992
- "items": [
993
- { "id": "guid", "code": "EMP001", "name": "John Doe", ... }
994
- ],
995
- "totalCount": 42
996
- }
997
- ```
998
-
999
- The backend service's `GetAllAsync` method should accept search parameters:
1000
-
1001
- ```csharp
1002
- public async Task<PaginatedResult<EntityResponseDto>> GetAllAsync(
1003
- string? search = null,
1004
- int page = 1,
1005
- int pageSize = 20,
1006
- CancellationToken ct = default)
1007
- {
1008
- var query = _db.Entities
1009
- .Where(x => x.TenantId == _currentUser.TenantId);
1010
-
1011
- if (!string.IsNullOrWhiteSpace(search))
1012
- {
1013
- query = query.Where(x =>
1014
- x.Name.Contains(search) ||
1015
- x.Code.Contains(search));
1016
- }
1017
-
1018
- var totalCount = await query.CountAsync(ct);
1019
- var items = await query
1020
- .OrderBy(x => x.Name)
1021
- .Skip((page - 1) * pageSize)
1022
- .Take(pageSize)
1023
- .Select(x => new EntityResponseDto { ... })
1024
- .ToListAsync(ct);
1025
-
1026
- return new PaginatedResult<EntityResponseDto>(items, totalCount, page, pageSize);
1027
- }
1028
- ```
1029
-
1030
- ### Rules
1031
-
1032
- - **NEVER** render a `Guid` FK field as `<input type="text">` — always use `EntityLookup`
1033
- - **NEVER** render a `Guid` FK field as `<select>` — even with API-loaded `<option>` elements, `<select>` is NOT acceptable
1034
- - **NEVER** ask the user to manually type or paste a GUID/ID
1035
- - **ALWAYS** provide a search-based selection via `<EntityLookup />` for FK fields
1036
- - **ALWAYS** show the entity's display name (Name, FullName, Code+Name) not the GUID
1037
- - **ALWAYS** include `mapOption` to define how the related entity is displayed
1038
- - **ALWAYS** load the selected entity's display name on mount (for edit forms)
1039
- - **ALWAYS** support clearing the selection (unless required + already set)
1040
-
1041
- **Why `<select>` is NOT acceptable for FK fields:**
1042
- - `<select>` loads ALL options at once — fails with 100+ entities (performance + UX)
1043
- - `<select>` has no search/filter — user must scroll through all options
1044
- - `<select>` cannot show sublabels (code, department, etc.)
1045
- - `EntityLookup` provides: debounced API search, paginated results, display name resolution, sublabels
1046
-
1047
- **FORBIDDEN:**
1048
- ```tsx
1049
- // WRONG: Plain text input for FK field
1050
- <input
1051
- type="text"
1052
- value={formData.employeeId}
1053
- onChange={(e) => handleChange('employeeId', e.target.value)}
1054
- placeholder="Enter Employee ID..."
1055
- />
1056
-
1057
- // WRONG: <select> dropdown for FK field (even with API-loaded options)
1058
- <select
1059
- value={formData.departmentId}
1060
- onChange={(e) => setFormData({ ...formData, departmentId: e.target.value })}
1061
- >
1062
- <option value="">Select Department...</option>
1063
- {departments.map((dept) => (
1064
- <option key={dept.id} value={dept.id}>{dept.name}</option>
1065
- ))}
1066
- </select>
1067
-
1068
- // WRONG: Raw GUID displayed to user
1069
- <span>{entity.departmentId}</span>
1070
-
1071
- // WRONG: Select with hardcoded options for FK
1072
- <select onChange={(e) => handleChange('departmentId', e.target.value)}>
1073
- <option value="guid-1">Department A</option>
1074
- </select>
1075
- ```
1076
-
1077
- **CORRECT — ONLY this pattern:**
1078
- ```tsx
1079
- <EntityLookup
1080
- apiEndpoint="/api/human-resources/departments"
1081
- value={formData.departmentId}
1082
- onChange={(id) => handleChange('departmentId', id)}
1083
- label={t('module:form.department', 'Department')}
1084
- mapOption={(dept) => ({ id: dept.id, label: dept.name, sublabel: dept.code })}
1085
- required
1086
- />
1087
- ```
1088
-
1089
- ### I18n Keys for EntityLookup
1090
-
1091
- Add these keys to the module's translation files:
1092
-
1093
- ```json
1094
- {
1095
- "form": {
1096
- "employee": "Employee",
1097
- "employeePlaceholder": "Search for an employee...",
1098
- "department": "Department",
1099
- "departmentPlaceholder": "Search for a department..."
1100
- }
1101
- }
1102
- ```
1103
-
1104
- ---
1105
-
1106
- ## 7. Documentation Panel Integration (DocToggleButton)
1107
-
1108
- > **EVERY list/detail page MUST include a `DocToggleButton` in its header.**
1109
- > This button opens the right-side documentation panel showing the module's user documentation.
1110
-
1111
- ### Component Import
1112
-
1113
- ```tsx
1114
- import { DocToggleButton } from '@/components/docs/DocToggleButton';
1115
- ```
1116
-
1117
- ### Placement — Always in the page header actions area (top right)
1118
-
1119
- ```tsx
1120
- {/* Header with DocToggleButton */}
1121
- <div className="flex items-center justify-between">
1122
- <h1 className="text-2xl font-bold text-[var(--text-primary)]">
1123
- {t('{module}:title', 'Module Title')}
1124
- </h1>
1125
- <div className="flex items-center gap-2">
1126
- <DocToggleButton />
1127
- <button onClick={() => navigate('create')} className="...">
1128
- {t('{module}:actions.create', 'Create')}
1129
- </button>
1130
- </div>
1131
- </div>
1132
- ```
1133
-
1134
- ### How it Works
1135
-
1136
- 1. `DocToggleButton` uses `useDocPanel()` context (provided by the Layout)
1137
- 2. On click → opens the `DocPanel` on the right side of the screen
1138
- 3. The panel loads the module's documentation via iframe (`?embedded=true`)
1139
- 4. Route → doc mapping is in `DocPanelContext.tsx` — maps current pathname to doc URL
1140
- 5. Panel is resizable (20-60% width), size persists in localStorage
1141
-
1142
- ### Documentation Generation
1143
-
1144
- After frontend pages are created, invoke the `/documentation` skill to generate:
1145
-
1146
- | File | Content |
1147
- |------|---------|
1148
- | `src/pages/docs/business/{app}/{module}/doc-data.ts` | Data-driven documentation (~50-80 lines) |
1149
- | `src/pages/docs/business/{app}/{module}/index.tsx` | Page wrapper (~10 lines) using `DocRenderer` |
1150
- | `src/i18n/locales/fr/docs-{app}-{module}.json` | French doc translations (source language) |
1151
-
1152
- The `DocRenderer` shared component renders all 8 documentation sections (overview, use cases, benefits, features, steps, FAQ, business rules, permissions, API endpoints) from the `doc-data.ts` file.
1153
-
1154
- ### Custom Doc URL (optional)
1155
-
1156
- If the automatic route mapping doesn't work for your module, pass a custom URL:
1157
-
1158
- ```tsx
1159
- <DocToggleButton customDocUrl="/docs/human-resources/employees" />
1160
- ```
1161
-
1162
- ### Rules
1163
-
1164
- - **EVERY** list page MUST include `DocToggleButton` in its header actions
1165
- - **EVERY** detail page MUST include `DocToggleButton` in its header actions
1166
- - Create/Edit form pages do NOT need DocToggleButton (users don't read docs while filling forms)
1167
- - DocToggleButton is imported from `@/components/docs/DocToggleButton` (shared component)
1168
- - The Layout already provides `DocPanelProvider` — no additional wrapping needed
1169
- - Documentation content is generated by the `/documentation` skill AFTER frontend pages exist
1170
-
1171
- ---
1172
-
1173
- ## 7b. Checklist for /apex Frontend Execution
1174
-
1175
- Before marking frontend tasks as complete, verify:
1176
-
1177
- - [ ] All page imports use `React.lazy()` with named export wrapping
1178
- - [ ] `<Suspense fallback={<PageLoader />}>` wraps all lazy components in routes
1179
- - [ ] Translation files exist for **all 4 languages** (fr, en, it, de) in `src/i18n/locales/`
1180
- - [ ] All `t()` calls include namespace prefix AND fallback value
1181
- - [ ] No hardcoded strings in JSX — all text goes through `t()`
1182
- - [ ] CSS uses variables only — no hardcoded Tailwind colors (BLOCKING POST-CHECK 13)
1183
- - [ ] Pages follow loading → error → content pattern
1184
- - [ ] Pages use `src/pages/{App}/{Module}/` hierarchy
1185
- - [ ] API calls use generated hooks or `apiClient` (never raw axios)
1186
- - [ ] Components use SmartTable/SmartFilter/EntityCard (never raw HTML tables)
1187
- - [ ] **FK fields use `EntityLookup` — ZERO plain text inputs for Guid FK fields**
1188
- - [ ] **All FK fields have `mapOption` showing display name, not GUID**
1189
- - [ ] **Backend APIs support `?search=` query parameter for EntityLookup**
1190
- - [ ] **Create/Edit forms are full pages with own routes — ZERO modals/popups/drawers**
1191
- - [ ] `EntityCreatePage.tsx` exists with route `/{module}/create`
1192
- - [ ] `EntityEditPage.tsx` exists with route `/{module}/:id/edit`
1193
- - [ ] No `<Modal>`, `<Dialog>`, `<Drawer>` imports in form-related pages
1194
- - [ ] Form pages include back button with `navigate(-1)`
1195
- - [ ] Form pages are covered by frontend tests (see section 8)
1196
- - [ ] **`DocToggleButton` present in header of every list/detail page (see section 7)**
1197
- - [ ] **`/documentation` skill invoked to generate module doc-data.ts**
1198
-
1199
- ---
1200
-
1201
- ## 7c. Cross-Tenant Entity UI Patterns
1202
-
1203
- > **For optional and scoped tenant entities, the frontend MUST provide UI controls to set the scope/visibility.**
1204
-
1205
- ### Scope Types
1206
-
1207
- | Type | Behavior | Use case |
1208
- |------|----------|----------|
1209
- | **Optional** | Entity can be tenant-specific OR shared (binary choice) | Data that can belong to one org or all orgs |
1210
- | **Scoped** | Entity has explicit scope enum: Tenant / Shared / Platform | Data with multiple visibility levels |
1211
-
1212
- ### Scope Selector in Create Forms (Optional Entities)
1213
-
1214
- For `optional` tenant entities, add a toggle in the create form allowing the user to decide:
1215
-
1216
- ```tsx
1217
- import { useState } from 'react';
1218
- import { useTranslation } from 'react-i18next';
1219
-
1220
- export function EntityCreatePage() {
1221
- const { t } = useTranslation(['{module}']);
1222
- const [formData, setFormData] = useState({
1223
- name: '',
1224
- isShared: false, // User decision: tenant-specific (false) or shared (true)
1225
- });
1226
-
1227
- const handleScopeChange = (value: string) => {
1228
- setFormData({ ...formData, isShared: value === 'shared' });
1229
- };
1230
-
1231
- return (
1232
- <div className="space-y-6">
1233
- {/* ... form header ... */}
1234
-
1235
- <SmartForm fields={[
1236
- {
1237
- name: 'name',
1238
- type: 'text',
1239
- label: t('{module}:form.name', 'Name'),
1240
- required: true,
1241
- },
1242
- // Scope selector — binary toggle for optional entities
1243
- {
1244
- name: 'scope',
1245
- type: 'custom',
1246
- label: t('common:scope', 'Scope'),
1247
- render: () => (
1248
- <div className="space-y-2">
1249
- <label className="block text-sm font-medium text-[var(--text-primary)]">
1250
- {t('common:scope', 'Scope')}
1251
- </label>
1252
- <select
1253
- value={formData.isShared ? 'shared' : 'tenant'}
1254
- onChange={(e) => handleScopeChange(e.target.value)}
1255
- className="w-full px-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)] text-[var(--text-primary)]"
1256
- >
1257
- <option value="tenant">
1258
- {t('common:scope.tenant', 'My Organization')}
1259
- </option>
1260
- <option value="shared">
1261
- {t('common:scope.shared', 'Shared (All Organizations)')}
1262
- </option>
1263
- </select>
1264
- <p className="text-xs text-[var(--text-secondary)]">
1265
- {formData.isShared
1266
- ? t('common:scope.shared.hint', 'This data will be accessible to all organizations')
1267
- : t('common:scope.tenant.hint', 'This data will only be visible to your organization')}
1268
- </p>
1269
- </div>
1270
- ),
1271
- },
1272
- ]} />
1273
- </div>
1274
- );
1275
- }
1276
- ```
1277
-
1278
- ### Scope Selector in Create Forms (Scoped Entities)
1279
-
1280
- For `scoped` entities with explicit enum values (Tenant, Shared, Platform), use a dropdown with all scope options:
1281
-
1282
- ```tsx
1283
- export function EntityCreatePage() {
1284
- const { t } = useTranslation(['{module}']);
1285
- const [formData, setFormData] = useState({
1286
- name: '',
1287
- scope: 'Tenant', // Enum: 'Tenant' | 'Shared' | 'Platform'
1288
- });
1289
-
1290
- return (
1291
- <SmartForm fields={[
1292
- {
1293
- name: 'name',
1294
- type: 'text',
1295
- label: t('{module}:form.name', 'Name'),
1296
- required: true,
1297
- },
1298
- {
1299
- name: 'scope',
1300
- type: 'select',
1301
- label: t('common:scope', 'Scope'),
1302
- options: [
1303
- { value: 'Tenant', label: t('common:scope.tenant', 'My Organization') },
1304
- { value: 'Shared', label: t('common:scope.shared', 'Shared') },
1305
- { value: 'Platform', label: t('common:scope.platform', 'Platform (Admin Only)') },
1306
- ],
1307
- default: 'Tenant',
1308
- required: true,
1309
- help: t('common:scope.help', 'Select the visibility level for this data'),
1310
- },
1311
- ]} />
1312
- );
1313
- }
1314
- ```
1315
-
1316
- ### Scope Indicator in List Views
1317
-
1318
- Display a visual indicator/badge on each row showing the entity scope:
1319
-
1320
- ```tsx
1321
- import { useTranslation } from 'react-i18next';
1322
-
1323
- // ScopeBadge component for reuse
1324
- interface ScopeBadgeProps {
1325
- tenantId?: string | null; // For optional entities: null = shared, value = tenant-specific
1326
- scope?: string; // For scoped entities: 'Tenant' | 'Shared' | 'Platform'
1327
- }
1328
-
1329
- export function ScopeBadge({ tenantId, scope }: ScopeBadgeProps) {
1330
- const { t } = useTranslation(['common']);
1331
-
1332
- // Optional entity scope
1333
- if (tenantId !== undefined) {
1334
- const isTenant = Boolean(tenantId);
1335
- return (
1336
- <span
1337
- className={`px-2 py-1 rounded-full text-xs font-semibold ${
1338
- isTenant
1339
- ? 'bg-[var(--bg-accent-light)] text-[var(--color-accent-600)]'
1340
- : 'bg-[var(--bg-secondary)] text-[var(--text-secondary)]'
1341
- }`}
1342
- >
1343
- {isTenant
1344
- ? t('common:scope.tenant', 'Tenant')
1345
- : t('common:scope.shared', 'Shared')}
1346
- </span>
1347
- );
1348
- }
1349
-
1350
- // Scoped entity scope
1351
- if (scope) {
1352
- const scopeStyles: Record<string, { bg: string; text: string }> = {
1353
- Tenant: {
1354
- bg: 'bg-[var(--bg-accent-light)]',
1355
- text: 'text-[var(--color-accent-600)]',
1356
- },
1357
- Shared: {
1358
- bg: 'bg-[var(--bg-secondary)]',
1359
- text: 'text-[var(--text-secondary)]',
1360
- },
1361
- Platform: {
1362
- bg: 'bg-[var(--bg-warning-light)]',
1363
- text: 'text-[var(--color-warning-600)]',
1364
- },
1365
- };
1366
-
1367
- const style = scopeStyles[scope] || scopeStyles.Tenant;
1368
- const scopeLabel = {
1369
- Tenant: t('common:scope.tenant', 'Organization'),
1370
- Shared: t('common:scope.shared', 'Shared'),
1371
- Platform: t('common:scope.platform', 'Platform'),
1372
- }[scope] || scope;
1373
-
1374
- return (
1375
- <span className={`px-2 py-1 rounded-full text-xs font-semibold ${style.bg} ${style.text}`}>
1376
- {scopeLabel}
1377
- </span>
1378
- );
1379
- }
1380
-
1381
- return null;
1382
- }
1383
- ```
1384
-
1385
- ### Using ScopeBadge in SmartTable Columns
1386
-
1387
- ```tsx
1388
- // In the list page, add a scope column
1389
- const columns = [
1390
- { key: 'name', label: t('{module}:columns.name', 'Name') },
1391
- { key: 'code', label: t('{module}:columns.code', 'Code') },
1392
- {
1393
- key: 'scope',
1394
- label: t('common:scope', 'Scope'),
1395
- render: (row) => (
1396
- // For optional entities: show based on tenantId
1397
- <ScopeBadge tenantId={row.tenantId} />
1398
- // OR for scoped entities: show based on scope field
1399
- // <ScopeBadge scope={row.scope} />
1400
- ),
1401
- },
1402
- { key: 'actions', label: t('{module}:columns.actions', 'Actions') },
1403
- ];
1404
-
1405
- return (
1406
- <SmartTable
1407
- columns={columns}
1408
- data={data}
1409
- loading={loading}
1410
- onRowClick={(row) => navigate(`${row.id}`)}
1411
- />
1412
- );
1413
- ```
1414
-
1415
- ### I18n Keys for Scope UI
1416
-
1417
- Add these keys to `src/i18n/locales/*/common.json`:
1418
-
1419
- ```json
1420
- {
1421
- "scope": "Scope",
1422
- "scope.tenant": "My Organization",
1423
- "scope.tenant.hint": "This data will only be visible to your organization",
1424
- "scope.shared": "Shared (All Organizations)",
1425
- "scope.shared.hint": "This data will be accessible to all organizations",
1426
- "scope.platform": "Platform (Admin Only)",
1427
- "scope.help": "Select the visibility level for this data"
1428
- }
1429
- ```
1430
-
1431
- And in the module-specific translation files (e.g., `employees.json`):
1432
-
1433
- ```json
1434
- {
1435
- "form": {
1436
- "scope": "Scope",
1437
- "scopeHint": "Choose who can see this data"
1438
- }
1439
- }
1440
- ```
1441
-
1442
- ### Rules
1443
-
1444
- - **ALWAYS** provide scope controls in create forms for optional/scoped entities
1445
- - **ALWAYS** show scope indicator badges in list views
1446
- - **ALWAYS** use `ScopeBadge` component for consistency across modules
1447
- - **NEVER** let users create shared entities without explicit choice
1448
- - **NEVER** hide scope controls — scope is a business-critical property
1449
- - **ALWAYS** include scope-related translation keys in i18n files (all 4 languages)
1450
- - **FORBIDDEN:** Form field for scope labeled ambiguously (e.g., "Public/Private" without context)
1451
- - **FORBIDDEN:** Scope badges with hardcoded colors — always use CSS variables
1452
-
1453
- ---
1454
-
1455
- ## 8. Frontend Form Testing
1456
-
1457
- > **ALL form pages MUST have tests.** Forms are critical user interaction points and MUST be verified.
1458
-
1459
- ### Required Test Coverage per Form Page
1460
-
1461
- | Test category | What to verify | Tool |
1462
- |---------------|---------------|------|
1463
- | Rendering | Form renders with all expected fields | Vitest + React Testing Library |
1464
- | Validation | Required fields show errors on empty submit | Vitest + React Testing Library |
1465
- | Submission | Successful submit calls API and navigates back | Vitest + MSW (mock API) |
1466
- | Pre-fill (edit) | Edit form loads entity data into fields | Vitest + React Testing Library |
1467
- | Navigation | Back button calls `navigate(-1)` | Vitest + React Testing Library |
1468
- | Error handling | API error displays error message | Vitest + MSW |
1469
-
1470
- ### Test File Convention
1471
-
1472
- ```
1473
- src/pages/{App}/{Module}/
1474
- ├── EntityCreatePage.tsx
1475
- ├── EntityCreatePage.test.tsx ← MANDATORY
1476
- ├── EntityEditPage.tsx
1477
- ├── EntityEditPage.test.tsx ← MANDATORY
1478
- ├── EntityListPage.tsx
1479
- └── EntityDetailPage.tsx
1480
- ```
1481
-
1482
- ### Create Page Test Template
1483
-
1484
- ```tsx
1485
- import { render, screen, waitFor } from '@testing-library/react';
1486
- import userEvent from '@testing-library/user-event';
1487
- import { MemoryRouter } from 'react-router-dom';
1488
- import { describe, it, expect, vi } from 'vitest';
1489
- import { EntityCreatePage } from './EntityCreatePage';
1490
-
1491
- // Mock API
1492
- vi.mock('@/services/api/apiClient');
1493
- const mockNavigate = vi.fn();
1494
- vi.mock('react-router-dom', async () => ({
1495
- ...(await vi.importActual('react-router-dom')),
1496
- useNavigate: () => mockNavigate,
1497
- }));
1498
-
1499
- describe('EntityCreatePage', () => {
1500
- it('renders the create form with all fields', () => {
1501
- render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
1502
- expect(screen.getByRole('textbox', { name: /name/i })).toBeInTheDocument();
1503
- // Verify all expected form fields
1504
- });
1505
-
1506
- it('shows validation errors on empty submit', async () => {
1507
- render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
1508
- await userEvent.click(screen.getByRole('button', { name: /save|create/i }));
1509
- await waitFor(() => {
1510
- expect(screen.getByText(/required/i)).toBeInTheDocument();
1511
- });
1512
- });
1513
-
1514
- it('submits form and navigates back on success', async () => {
1515
- render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
1516
- await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'Test');
1517
- await userEvent.click(screen.getByRole('button', { name: /save|create/i }));
1518
- await waitFor(() => {
1519
- expect(mockNavigate).toHaveBeenCalledWith(-1);
1520
- });
1521
- });
1522
-
1523
- it('navigates back on cancel/back button', async () => {
1524
- render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
1525
- await userEvent.click(screen.getByRole('button', { name: /back|cancel/i }));
1526
- expect(mockNavigate).toHaveBeenCalledWith(-1);
1527
- });
1528
- });
1529
- ```
1530
-
1531
- ### Edit Page Test Template
1532
-
1533
- ```tsx
1534
- describe('EntityEditPage', () => {
1535
- it('loads entity data and pre-fills the form', async () => {
1536
- render(<MemoryRouter initialEntries={['/entities/123/edit']}><EntityEditPage /></MemoryRouter>);
1537
- await waitFor(() => {
1538
- expect(screen.getByDisplayValue('Existing Name')).toBeInTheDocument();
1539
- });
1540
- });
1541
-
1542
- it('submits updated data and navigates back', async () => {
1543
- render(<MemoryRouter initialEntries={['/entities/123/edit']}><EntityEditPage /></MemoryRouter>);
1544
- await waitFor(() => screen.getByDisplayValue('Existing Name'));
1545
- await userEvent.clear(screen.getByRole('textbox', { name: /name/i }));
1546
- await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'Updated');
1547
- await userEvent.click(screen.getByRole('button', { name: /save/i }));
1548
- await waitFor(() => {
1549
- expect(mockNavigate).toHaveBeenCalledWith(-1);
1550
- });
1551
- });
1552
-
1553
- it('displays error when API call fails', async () => {
1554
- // Mock API to reject
1555
- render(<MemoryRouter initialEntries={['/entities/123/edit']}><EntityEditPage /></MemoryRouter>);
1556
- // ... trigger submit with mocked failure
1557
- await waitFor(() => {
1558
- expect(screen.getByText(/failed/i)).toBeInTheDocument();
1559
- });
1560
- });
1561
- });
1562
- ```
1563
-
1564
- ### Rules
1565
-
1566
- - **EVERY** `EntityCreatePage.tsx` MUST have a companion `EntityCreatePage.test.tsx`
1567
- - **EVERY** `EntityEditPage.tsx` MUST have a companion `EntityEditPage.test.tsx`
1568
- - Tests MUST cover: rendering, validation, submit success, submit error, navigation
1569
- - Use `@testing-library/react` + `@testing-library/user-event` (NEVER enzyme)
1570
- - Mock API with `vi.mock()` or MSW — NEVER make real API calls in tests
1571
- - Test files live next to their component (co-located, NOT in a separate `__tests__/` folder)
1
+ # SmartStack Frontend Patterns — Mandatory Reference
2
+
3
+ > **Loaded by:** step-03 (execution) and step-04 (validation)
4
+ > **Purpose:** Defines mandatory frontend patterns extracted from SmartStack.app.
5
+ > **Enforcement:** POST-CHECKs in step-04 verify compliance.
6
+
7
+ ---
8
+
9
+ ## 1. Lazy Loading (React.lazy + Suspense)
10
+
11
+ > **ALL page components MUST be lazy-loaded.** Only critical entry pages (HomePage, LoginPage) may use static imports.
12
+
13
+ ### Import Pattern
14
+
15
+ ```tsx
16
+ // Named exports — use .then() to wrap
17
+ const EmployeesPage = lazy(() =>
18
+ import('@/pages/HumanResources/Employees/EmployeesPage')
19
+ .then(m => ({ default: m.EmployeesPage }))
20
+ );
21
+
22
+ // Default exports — direct lazy
23
+ const DashboardPage = lazy(() => import('@/pages/Platform/Admin/DashboardPage'));
24
+ ```
25
+
26
+ ### Suspense Wrapper
27
+
28
+ ```tsx
29
+ import { Suspense } from 'react';
30
+ import { PageLoader } from '@/components/ui/PageLoader';
31
+
32
+ // Route element wrapping
33
+ element: (
34
+ <Suspense fallback={<PageLoader />}>
35
+ <PermissionGuard permissions={ROUTES['hr.employees'].permissions}>
36
+ <EmployeesPage />
37
+ </PermissionGuard>
38
+ </Suspense>
39
+ )
40
+ ```
41
+
42
+ ### Rules
43
+
44
+ - **NEVER** static-import page components in route files
45
+ - **ALWAYS** use `<Suspense fallback={<PageLoader />}>` around lazy components
46
+ - **ALWAYS** use the `.then(m => ({ default: m.ComponentName }))` pattern for named exports
47
+ - The unified AppLayout component is ALSO lazy-loaded
48
+
49
+ **FORBIDDEN:**
50
+ ```tsx
51
+ // WRONG: static import in route file
52
+ import { EmployeesPage } from '@/pages/HumanResources/Employees/EmployeesPage';
53
+
54
+ // WRONG: no Suspense wrapper
55
+ element: <EmployeesPage />
56
+
57
+ // WRONG: no fallback
58
+ <Suspense><EmployeesPage /></Suspense>
59
+ ```
60
+
61
+ ### Client App.tsx — Lazy Imports Mandatory
62
+
63
+ > **CRITICAL:** In the client `App.tsx` (where application routes are defined), ALL page imports MUST use `React.lazy()`.
64
+
65
+ **CORRECT — Lazy imports in client App.tsx:**
66
+ ```tsx
67
+ const ClientsListPage = lazy(() =>
68
+ import('@/pages/HumanResources/Clients/ClientsListPage')
69
+ .then(m => ({ default: m.ClientsListPage }))
70
+ );
71
+ ```
72
+
73
+ **FORBIDDEN — Static imports in client App.tsx:**
74
+ ```tsx
75
+ // WRONG: Static import kills code splitting
76
+ import { ClientsListPage } from '@/pages/HumanResources/Clients/ClientsListPage';
77
+ ```
78
+
79
+ > **Note:** The `smartstackRoutes.tsx` from the npm package may use static imports internally — this is acceptable for the package. But client `App.tsx` code MUST always use lazy imports for business pages.
80
+
81
+ ---
82
+
83
+ ## 2. I18n / Translations (react-i18next)
84
+
85
+ > **ALL user-facing text MUST use translations.** 4 languages required: fr, en, it, de.
86
+
87
+ ### File Structure
88
+
89
+ ```
90
+ src/i18n/
91
+ ├── config.ts # i18n initialization
92
+ ├── locales/
93
+ │ ├── fr/
94
+ │ │ ├── common.json # Shared keys (actions, errors, validation)
95
+ │ │ ├── navigation.json # Menu labels
96
+ │ │ └── {module}.json # Module-specific keys
97
+ │ ├── en/
98
+ │ │ └── {module}.json
99
+ │ ├── it/
100
+ │ │ └── {module}.json
101
+ │ └── de/
102
+ │ └── {module}.json
103
+ ```
104
+
105
+ ### Module JSON Template
106
+
107
+ Each new module MUST generate a translation file with this structure:
108
+
109
+ ```json
110
+ {
111
+ "title": "Module display name",
112
+ "description": "Module description",
113
+ "actions": {
114
+ "create": "Create {entity}",
115
+ "edit": "Edit {entity}",
116
+ "delete": "Delete {entity}",
117
+ "save": "Save",
118
+ "cancel": "Cancel",
119
+ "search": "Search...",
120
+ "export": "Export",
121
+ "refresh": "Refresh"
122
+ },
123
+ "labels": {
124
+ "name": "Name",
125
+ "code": "Code",
126
+ "description": "Description",
127
+ "status": "Status",
128
+ "createdAt": "Created at",
129
+ "updatedAt": "Updated at",
130
+ "createdBy": "Created by",
131
+ "isActive": "Active"
132
+ },
133
+ "columns": {
134
+ "name": "Name",
135
+ "code": "Code",
136
+ "status": "Status",
137
+ "actions": "Actions"
138
+ },
139
+ "form": {
140
+ "name": "Name",
141
+ "namePlaceholder": "Enter name...",
142
+ "code": "Code",
143
+ "codePlaceholder": "Enter code...",
144
+ "description": "Description",
145
+ "descriptionPlaceholder": "Enter description..."
146
+ },
147
+ "errors": {
148
+ "loadFailed": "Failed to load data",
149
+ "saveFailed": "Failed to save",
150
+ "deleteFailed": "Failed to delete",
151
+ "notFound": "Not found",
152
+ "permissionDenied": "Permission denied"
153
+ },
154
+ "validation": {
155
+ "nameRequired": "Name is required",
156
+ "codeRequired": "Code is required",
157
+ "nameMaxLength": "Name must be less than {{max}} characters"
158
+ },
159
+ "messages": {
160
+ "created": "{entity} created successfully",
161
+ "updated": "{entity} updated successfully",
162
+ "deleted": "{entity} deleted successfully",
163
+ "confirmDelete": "Are you sure you want to delete this {entity}?"
164
+ },
165
+ "empty": {
166
+ "title": "No {entity} found",
167
+ "description": "Create your first {entity} to get started"
168
+ }
169
+ }
170
+ ```
171
+
172
+ ### Usage in Components
173
+
174
+ ```tsx
175
+ // Hook — specify namespace(s)
176
+ const { t } = useTranslation(['employees']);
177
+
178
+ // Simple key with MANDATORY fallback
179
+ t('employees:title', 'Employees')
180
+
181
+ // Key with interpolation
182
+ t('employees:messages.created', '{{entity}} created successfully', { entity: 'Employee' })
183
+
184
+ // Namespace prefix syntax
185
+ t('employees:actions.create', 'Create employee')
186
+ t('common:actions.save', 'Save')
187
+ t('common:errors.network', 'Network error')
188
+ ```
189
+
190
+ ### Namespace Registration (CRITICAL)
191
+
192
+ > **After creating i18n JSON files, you MUST register each namespace in the i18n config.**
193
+ > Root cause (test-apex-007): JSON files existed but namespaces were not registered → `useTranslation(['module'])` returned empty strings.
194
+
195
+ In the i18n config file (`src/i18n/config.ts` or `src/i18n/index.ts`), add each new namespace:
196
+
197
+ ```typescript
198
+ // Example: registering new module namespaces
199
+ import employees from './locales/fr/employees.json';
200
+ import projects from './locales/fr/projects.json';
201
+ import clients from './locales/fr/clients.json';
202
+
203
+ // In resources configuration:
204
+ resources: {
205
+ fr: { employees, projects, clients, common, navigation },
206
+ en: { employees: employeesEn, projects: projectsEn, clients: clientsEn, ... },
207
+ // ... it, de
208
+ }
209
+
210
+ // OR with ns array:
211
+ ns: ['common', 'navigation', 'employees', 'projects', 'clients'],
212
+ ```
213
+
214
+ POST-CHECK 45 validates this. Unregistered namespaces → BLOCKING.
215
+
216
+ ### Rules
217
+
218
+ - **ALWAYS** provide a fallback value as 2nd argument to `t()`
219
+ - **ALWAYS** use namespace prefix: `t('namespace:key')`
220
+ - **ALWAYS** generate 4 language files (fr, en, it, de) with identical key structures
221
+ - **ALWAYS** register new namespaces in i18n config file after creating JSON files
222
+ - **NEVER** hardcode user-facing strings in JSX
223
+ - **NEVER** use `t('key')` without namespace prefix
224
+
225
+ **FORBIDDEN:**
226
+ ```tsx
227
+ // WRONG: no fallback
228
+ t('employees:title')
229
+
230
+ // WRONG: no namespace
231
+ t('title')
232
+
233
+ // WRONG: hardcoded text
234
+ <h1>Employees</h1>
235
+
236
+ // WRONG: only 2 languages generated
237
+ // Must have fr, en, it, de
238
+ ```
239
+
240
+ ---
241
+
242
+ ## 3. Page Structure Pattern
243
+
244
+ > **ALL pages MUST follow this structure.** Extracted from SmartStack.app reference implementation.
245
+
246
+ ### Standard List Page Template
247
+
248
+ ```tsx
249
+ import { useState, useCallback, useEffect } from 'react';
250
+ import { useTranslation } from 'react-i18next';
251
+ import { useNavigate, useParams } from 'react-router-dom';
252
+ import { Loader2 } from 'lucide-react';
253
+ import { DocToggleButton } from '@/components/docs/DocToggleButton';
254
+
255
+ // API hook (generated by scaffold_api_client)
256
+ import { useEntityList } from '@/hooks/useEntity';
257
+
258
+ export function EntityListPage() {
259
+ // 1. HOOKS — always at the top
260
+ const { t } = useTranslation(['{module}']);
261
+ const navigate = useNavigate();
262
+
263
+ // 2. STATE
264
+ const [loading, setLoading] = useState(true);
265
+ const [error, setError] = useState<string | null>(null);
266
+ const [data, setData] = useState<Entity[]>([]);
267
+
268
+ // 3. DATA LOADING (useCallback + useEffect)
269
+ const loadData = useCallback(async () => {
270
+ try {
271
+ setLoading(true);
272
+ setError(null);
273
+ const result = await entityApi.getAll();
274
+ setData(result.items);
275
+ } catch (err: any) {
276
+ setError(err.message || t('{module}:errors.loadFailed', 'Failed to load data'));
277
+ } finally {
278
+ setLoading(false);
279
+ }
280
+ }, [t]);
281
+
282
+ useEffect(() => {
283
+ loadData();
284
+ }, [loadData]);
285
+
286
+ // 4. LOADING STATE
287
+ if (loading) {
288
+ return (
289
+ <div className="flex items-center justify-center min-h-[400px]">
290
+ <Loader2 className="w-8 h-8 animate-spin text-[var(--color-accent-500)]" />
291
+ </div>
292
+ );
293
+ }
294
+
295
+ // 5. ERROR STATE
296
+ if (error) {
297
+ return (
298
+ <div className="flex items-center justify-center min-h-[400px]">
299
+ <div className="text-center">
300
+ <p className="text-[var(--text-secondary)]">{error}</p>
301
+ <button
302
+ onClick={loadData}
303
+ className="mt-4 px-4 py-2 bg-[var(--color-accent-500)] text-white rounded"
304
+ >
305
+ {t('common:actions.retry', 'Retry')}
306
+ </button>
307
+ </div>
308
+ </div>
309
+ );
310
+ }
311
+
312
+ // 6. CONTENT — create button navigates to /create route
313
+ return (
314
+ <div className="space-y-6">
315
+ {/* Header with DocToggleButton */}
316
+ <div className="flex items-center justify-between">
317
+ <h1 className="text-2xl font-bold text-[var(--text-primary)]">
318
+ {t('{module}:title', 'Module Title')}
319
+ </h1>
320
+ <div className="flex items-center gap-2">
321
+ <DocToggleButton />
322
+ <button
323
+ onClick={() => navigate('create')}
324
+ className="px-4 py-2 bg-[var(--color-accent-500)] text-white rounded"
325
+ >
326
+ {t('{module}:actions.create', 'Create')}
327
+ </button>
328
+ </div>
329
+ </div>
330
+
331
+ {/* Content: SmartTable with row click → detail, edit action → /:id/edit */}
332
+ </div>
333
+ );
334
+ }
335
+ ```
336
+
337
+ ### Detail Page Pattern
338
+
339
+ ```tsx
340
+ export function EntityDetailPage() {
341
+ const { entityId } = useParams<{ entityId: string }>();
342
+ const { t } = useTranslation(['{module}']);
343
+ const navigate = useNavigate();
344
+
345
+ const [entity, setEntity] = useState<Entity | null>(null);
346
+ const [loading, setLoading] = useState(true);
347
+ const [activeTab, setActiveTab] = useState('info');
348
+
349
+ // Lazy tab loading — load data only when tab is first visited
350
+ const visitedTabsRef = useRef<Set<string>>(new Set(['info']));
351
+
352
+ useEffect(() => {
353
+ if (!visitedTabsRef.current.has(activeTab)) {
354
+ visitedTabsRef.current.add(activeTab);
355
+ // Load tab-specific data here (e.g., fetch leaves for this employee)
356
+ }
357
+ }, [activeTab]);
358
+
359
+ // Edit button navigates to /:id/edit route (NEVER opens a modal)
360
+ const handleEdit = () => navigate(`edit`);
361
+
362
+ // ... loading/error/content pattern
363
+ }
364
+ ```
365
+
366
+ ### Tab Behavior Rules (CRITICAL)
367
+
368
+ > **CRITICAL: Tabs on detail pages switch content LOCALLY — they NEVER navigate to other pages.**
369
+ > Each tab renders its content INLINE within the same page component.
370
+ > Sub-resource data (e.g., an employee's leaves) loads via API call filtered by the parent entity ID.
371
+
372
+ **Tab state management:**
373
+ - Tabs use `useState<TabKey>('info')` for the active tab — LOCAL React state only
374
+ - Tab click handler: `onClick={() => setActiveTab(tabKey)}` — NEVER `navigate()`
375
+ - Tab content: conditional rendering `{activeTab === 'tabKey' && <TabContent />}`
376
+ - Lazy loading: `visitedTabsRef` tracks which tabs have been visited to avoid redundant API calls
377
+
378
+ **Tab content for sub-resources:**
379
+ ```tsx
380
+ // CORRECT — sub-resource data loaded INLINE within the tab
381
+ {activeTab === 'leaves' && (
382
+ <div>
383
+ <LeaveRequestsTable employeeId={entity.id} />
384
+ {/* Optional "View all" link INSIDE the tab content area */}
385
+ <Link to={`../leaves?employee=${entity.id}`}>
386
+ {t('employees:tabs.viewAllLeaves', 'View all leave requests')}
387
+ </Link>
388
+ </div>
389
+ )}
390
+ ```
391
+
392
+ **FORBIDDEN tab patterns:**
393
+ ```tsx
394
+ // FORBIDDEN — tab click handler navigates to another page
395
+ const handleTabClick = (tab: TabKey) => {
396
+ setActiveTab(tab);
397
+ if (tab === 'leaves') navigate(`../leaves?employee=${id}`); // ← BREAKS tab UX
398
+ };
399
+
400
+ // FORBIDDEN — tab content is empty because navigation already left the page
401
+ {activeTab === 'info' && <div>...</div>}
402
+ // Leaves tab: nothing renders here, user is already on another page
403
+ ```
404
+
405
+ **Why this matters:**
406
+ - Navigating away loses the detail page context (entity data, scroll position, other tab state)
407
+ - Users expect tabs to switch content in-place, not redirect to a different page
408
+ - The browser back button should go to the list page, not toggle between tabs
409
+
410
+ **POST-CHECK 43 enforces this rule.**
411
+
412
+ ---
413
+
414
+ ## 3b. Form Pages Pattern (Create / Edit)
415
+
416
+ > **CRITICAL: ALL forms MUST be full pages with their own URL route.**
417
+ > **NEVER use modals, dialogs, drawers, or popups for create/edit forms.**
418
+
419
+ ### Route Convention
420
+
421
+ > **CRITICAL:** Route paths MUST use **kebab-case** matching the navigation seed data (which uses `ToKebabCase()`).
422
+ > - Single word: `employees` (no change needed)
423
+ > - Multi-word: `human-resources`, `time-management` (kebab-case with hyphens)
424
+ > - **FORBIDDEN:** `humanresources`, `timemanagement` (concatenated words without hyphens)
425
+
426
+ | Action | Route pattern | Page component | File location |
427
+ |--------|--------------|----------------|---------------|
428
+ | Create | `/{module}/create` | `EntityCreatePage` | `src/pages/{App}/{Module}/EntityCreatePage.tsx` |
429
+ | Edit | `/{module}/:id/edit` | `EntityEditPage` | `src/pages/{App}/{Module}/EntityEditPage.tsx` |
430
+
431
+ ### Create Page Template
432
+
433
+ ```tsx
434
+ import { useState } from 'react';
435
+ import { useTranslation } from 'react-i18next';
436
+ import { useNavigate } from 'react-router-dom';
437
+
438
+ export function EntityCreatePage() {
439
+ const { t } = useTranslation(['{module}']);
440
+ const navigate = useNavigate();
441
+ const [submitting, setSubmitting] = useState(false);
442
+
443
+ const handleSubmit = async (data: CreateEntityDto) => {
444
+ try {
445
+ setSubmitting(true);
446
+ await entityApi.create(data);
447
+ navigate(-1); // Back to list
448
+ } catch (err: any) {
449
+ // Handle validation errors
450
+ } finally {
451
+ setSubmitting(false);
452
+ }
453
+ };
454
+
455
+ return (
456
+ <div className="space-y-6">
457
+ {/* Back button */}
458
+ <button
459
+ onClick={() => navigate(-1)}
460
+ className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
461
+ >
462
+ {t('common:actions.back', 'Back')}
463
+ </button>
464
+
465
+ {/* Page title */}
466
+ <h1 className="text-2xl font-bold text-[var(--text-primary)]">
467
+ {t('{module}:actions.create', 'Create {Entity}')}
468
+ </h1>
469
+
470
+ {/* SmartForm — NEVER in a modal */}
471
+ <SmartForm
472
+ fields={formFields}
473
+ onSubmit={handleSubmit}
474
+ onCancel={() => navigate(-1)}
475
+ submitting={submitting}
476
+ />
477
+ </div>
478
+ );
479
+ }
480
+ ```
481
+
482
+ ### Edit Page Template
483
+
484
+ ```tsx
485
+ import { useState, useEffect, useCallback } from 'react';
486
+ import { useTranslation } from 'react-i18next';
487
+ import { useNavigate, useParams } from 'react-router-dom';
488
+ import { Loader2 } from 'lucide-react';
489
+
490
+ export function EntityEditPage() {
491
+ const { entityId } = useParams<{ entityId: string }>();
492
+ const { t } = useTranslation(['{module}']);
493
+ const navigate = useNavigate();
494
+ const [entity, setEntity] = useState<Entity | null>(null);
495
+ const [loading, setLoading] = useState(true);
496
+ const [submitting, setSubmitting] = useState(false);
497
+
498
+ const loadEntity = useCallback(async () => {
499
+ try {
500
+ setLoading(true);
501
+ const result = await entityApi.getById(entityId!);
502
+ setEntity(result);
503
+ } catch {
504
+ navigate(-1);
505
+ } finally {
506
+ setLoading(false);
507
+ }
508
+ }, [entityId, navigate]);
509
+
510
+ useEffect(() => { loadEntity(); }, [loadEntity]);
511
+
512
+ if (loading) {
513
+ return (
514
+ <div className="flex items-center justify-center min-h-[400px]">
515
+ <Loader2 className="w-8 h-8 animate-spin text-[var(--color-accent-500)]" />
516
+ </div>
517
+ );
518
+ }
519
+
520
+ const handleSubmit = async (data: UpdateEntityDto) => {
521
+ try {
522
+ setSubmitting(true);
523
+ await entityApi.update(entityId!, data);
524
+ navigate(-1); // Back to detail or list
525
+ } catch (err: any) {
526
+ // Handle validation errors
527
+ } finally {
528
+ setSubmitting(false);
529
+ }
530
+ };
531
+
532
+ return (
533
+ <div className="space-y-6">
534
+ {/* Back button */}
535
+ <button
536
+ onClick={() => navigate(-1)}
537
+ className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
538
+ >
539
+ {t('common:actions.back', 'Back')}
540
+ </button>
541
+
542
+ {/* Page title */}
543
+ <h1 className="text-2xl font-bold text-[var(--text-primary)]">
544
+ {t('{module}:actions.edit', 'Edit {Entity}')}
545
+ </h1>
546
+
547
+ {/* SmartForm pre-filled — NEVER in a modal */}
548
+ <SmartForm
549
+ fields={formFields}
550
+ initialValues={entity}
551
+ onSubmit={handleSubmit}
552
+ onCancel={() => navigate(-1)}
553
+ submitting={submitting}
554
+ />
555
+ </div>
556
+ );
557
+ }
558
+ ```
559
+
560
+ ### Lazy Loading for Form Pages
561
+
562
+ ```tsx
563
+ // In route files — form pages are also lazy-loaded
564
+ const EntityCreatePage = lazy(() =>
565
+ import('@/pages/HumanResources/Employees/EntityCreatePage')
566
+ .then(m => ({ default: m.EntityCreatePage }))
567
+ );
568
+ const EntityEditPage = lazy(() =>
569
+ import('@/pages/HumanResources/Employees/EntityEditPage')
570
+ .then(m => ({ default: m.EntityEditPage }))
571
+ );
572
+
573
+ // Route registration — form pages have their own routes
574
+ {
575
+ path: 'employees',
576
+ children: [
577
+ { index: true, element: <Suspense fallback={<PageLoader />}><EmployeesPage /></Suspense> },
578
+ { path: 'create', element: <Suspense fallback={<PageLoader />}><EntityCreatePage /></Suspense> },
579
+ { path: ':id', element: <Suspense fallback={<PageLoader />}><EntityDetailPage /></Suspense> },
580
+ { path: ':id/edit', element: <Suspense fallback={<PageLoader />}><EntityEditPage /></Suspense> },
581
+ ]
582
+ }
583
+
584
+ // Section-level routes — children of the module route (when module has sections)
585
+ //
586
+ // > **IMPORTANT:** The `list` and `detail` sections do NOT generate additional route entries.
587
+ // > They are already covered by the module's `index: true` (list) and `path: ':id'` (detail) routes above.
588
+ // > Only sections like `dashboard`, `approve`, `import`, etc. generate the section-kebab child routes below.
589
+ // > FORBIDDEN: `path: 'list'`, `path: 'detail'` — these would create unreachable duplicate routes.
590
+ //
591
+ {
592
+ path: '{module-kebab}',
593
+ children: [
594
+ { index: true, element: <Suspense fallback={<PageLoader />}><{Module}Page /></Suspense> },
595
+ { path: 'create', element: <Suspense fallback={<PageLoader />}><Create{Module}Page /></Suspense> },
596
+ { path: ':id', element: <Suspense fallback={<PageLoader />}><{Module}DetailPage /></Suspense> },
597
+ { path: ':id/edit', element: <Suspense fallback={<PageLoader />}><Edit{Module}Page /></Suspense> },
598
+ // Section routes as children of module:
599
+ // IMPORTANT: "list" and "detail" are NOT separate path segments.
600
+ // - "list" section = already handled by the module's index route above (index: true)
601
+ // - "detail" section = already handled by the module's :id route above (path: ':id')
602
+ // - Only OTHER sections (dashboard, approve, import, etc.) add path segments:
603
+ { path: '{section-kebab}', element: <Suspense fallback={<PageLoader />}><{Section}Page /></Suspense> },
604
+ { path: '{section-kebab}/create', element: <Suspense fallback={<PageLoader />}><Create{Section}Page /></Suspense> },
605
+ { path: '{section-kebab}/:id', element: <Suspense fallback={<PageLoader />}><{Section}DetailPage /></Suspense> },
606
+ { path: '{section-kebab}/:id/edit', element: <Suspense fallback={<PageLoader />}><Edit{Section}Page /></Suspense> },
607
+ ]
608
+ }
609
+
610
+ // PermissionGuard for section-level routes
611
+ element: (
612
+ <Suspense fallback={<PageLoader />}>
613
+ <PermissionGuard permissions={ROUTES['app.module.section'].permissions}>
614
+ <SectionPage />
615
+ </PermissionGuard>
616
+ </Suspense>
617
+ )
618
+ ```
619
+
620
+ ### Rules
621
+
622
+ - **NEVER** use `<Modal>`, `<Dialog>`, `<Drawer>`, or `<Popup>` for create/edit forms
623
+ - **NEVER** use `useState(isOpen)` to toggle form visibility — forms are pages, not overlays
624
+ - **ALWAYS** create a dedicated `EntityCreatePage.tsx` and `EntityEditPage.tsx` page component
625
+ - **ALWAYS** register create/edit routes alongside list/detail routes
626
+ - **ALWAYS** use `navigate('create')` or `navigate(\`${id}/edit\`)` from list/detail pages
627
+ - **ALWAYS** include a back button that uses `navigate(-1)` to return to previous page
628
+
629
+ **FORBIDDEN:**
630
+ ```tsx
631
+ // WRONG: modal for create form
632
+ const [showCreateModal, setShowCreateModal] = useState(false);
633
+ <Modal open={showCreateModal}><CreateForm /></Modal>
634
+
635
+ // WRONG: dialog for edit form
636
+ <Dialog open={editDialogOpen}><EditForm entity={selected} /></Dialog>
637
+
638
+ // WRONG: drawer for form
639
+ <Drawer open={isDrawerOpen}><SmartForm /></Drawer>
640
+
641
+ // WRONG: inline form toggle
642
+ {isEditing ? <EditForm /> : <DetailView />}
643
+ ```
644
+
645
+ ---
646
+
647
+ ## 4. CSS Variables (Theme System)
648
+
649
+ > **NEVER use hardcoded Tailwind colors.** ALWAYS use CSS variables for theme support.
650
+
651
+ ### Variable Reference
652
+
653
+ | Usage | CSS Variable | Example |
654
+ |-------|-------------|---------|
655
+ | Background | `var(--bg-primary)` | `bg-[var(--bg-primary)]` |
656
+ | Card background | `var(--bg-card)` | `bg-[var(--bg-card)]` |
657
+ | Text primary | `var(--text-primary)` | `text-[var(--text-primary)]` |
658
+ | Text secondary | `var(--text-secondary)` | `text-[var(--text-secondary)]` |
659
+ | Borders | `var(--border-color)` | `border-[var(--border-color)]` |
660
+ | Accent | `var(--color-accent-500)` | `text-[var(--color-accent-500)]` |
661
+ | Card radius | `var(--radius-card)` | `style={{ borderRadius: 'var(--radius-card)' }}` |
662
+
663
+ ### Card Pattern
664
+
665
+ ```tsx
666
+ <div
667
+ className="bg-[var(--bg-card)] border border-[var(--border-color)] p-6"
668
+ style={{ borderRadius: 'var(--radius-card)' }}
669
+ >
670
+ <h2 className="text-lg font-semibold text-[var(--text-primary)]">Title</h2>
671
+ <p className="text-sm text-[var(--text-secondary)]">Description</p>
672
+ </div>
673
+ ```
674
+
675
+ **FORBIDDEN:**
676
+ ```tsx
677
+ // WRONG: hardcoded Tailwind colors
678
+ className="bg-white border-gray-200 text-gray-900"
679
+
680
+ // WRONG: hardcoded hex/rgb
681
+ style={{ backgroundColor: '#ffffff', color: '#1a1a1a' }}
682
+ ```
683
+
684
+ ---
685
+
686
+ ## 5. Component Rules
687
+
688
+ | Need | Component | Source |
689
+ |------|-----------|--------|
690
+ | Data table | `SmartTable` | `@/components/SmartTable` |
691
+ | Filters | `SmartFilter` | `@/components/SmartFilter` |
692
+ | Entity cards | `EntityCard` | `@/components/EntityCard` |
693
+ | Forms | `SmartForm` | `@/components/SmartForm` |
694
+ | FK field lookup | `EntityLookup` | `@/components/ui/EntityLookup` |
695
+ | Statistics | `StatCard` | `@/components/StatCard` |
696
+ | Loading spinner | `Loader2` | `lucide-react` |
697
+ | Page loader | `PageLoader` | `@/components/ui/PageLoader` |
698
+
699
+ ### Rules
700
+
701
+ - **NEVER** use raw `<table>` — use SmartTable
702
+ - **NEVER** create custom spinners — use `Loader2` from lucide-react
703
+ - **NEVER** import axios directly — use `@/services/api/apiClient`
704
+ - **ALWAYS** use `PageLoader` as Suspense fallback
705
+ - **ALWAYS** use existing shared components before creating new ones
706
+
707
+ ---
708
+
709
+ ## 6. Foreign Key Fields & Entity Lookup (CRITICAL)
710
+
711
+ > **NEVER render a foreign key (Guid) as a plain text input.** FK fields MUST use a searchable lookup component.
712
+ > A form asking the user to type a GUID manually is a UX failure. ALL FK fields must provide entity search & selection.
713
+
714
+ ### Field Type Classification
715
+
716
+ When generating form fields, determine the field type from the entity property:
717
+
718
+ | Property type | Form field type | Component |
719
+ |---------------|----------------|-----------|
720
+ | `string` | Text input | `<input type="text" />` |
721
+ | `string?` | Text input (optional) | `<input type="text" />` |
722
+ | `Guid` (FK — e.g., `EmployeeId`, `DepartmentId`) | **Entity Lookup** | `<EntityLookup />` |
723
+ | `bool` | Toggle/Checkbox | `<input type="checkbox" />` |
724
+ | `int` / `decimal` | Number input | `<input type="number" />` |
725
+ | `DateTime` | Date picker | `<input type="date" />` |
726
+ | `enum` | Select dropdown | `<select>` |
727
+
728
+ **How to detect FK fields:** Any property named `{Entity}Id` of type `Guid` that has a corresponding navigation property is a foreign key. Examples: `EmployeeId`, `DepartmentId`, `CategoryId`, `ParentId`.
729
+
730
+ ### EntityLookup Component Pattern
731
+
732
+ ```tsx
733
+ import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
734
+ import { useTranslation } from 'react-i18next';
735
+ import { Search, X, ChevronDown } from 'lucide-react';
736
+ import { apiClient } from '@/services/api/apiClient';
737
+
738
+ interface EntityLookupOption {
739
+ id: string;
740
+ label: string; // Display name (e.g., employee full name)
741
+ sublabel?: string; // Secondary info (e.g., department, code)
742
+ }
743
+
744
+ interface EntityLookupProps {
745
+ /** API endpoint to search entities (e.g., '/api/human-resources/employees') */
746
+ apiEndpoint: string;
747
+ /** Currently selected entity ID */
748
+ value: string | null;
749
+ /** Callback when entity is selected */
750
+ onChange: (id: string | null) => void;
751
+ /** Field label */
752
+ label: string;
753
+ /** Placeholder text */
754
+ placeholder?: string;
755
+ /** Map API response item to display option */
756
+ mapOption: (item: any) => EntityLookupOption;
757
+ /** Whether the field is required */
758
+ required?: boolean;
759
+ /** Whether the field is disabled */
760
+ disabled?: boolean;
761
+ /** Error message to display */
762
+ error?: string;
763
+ }
764
+
765
+ export function EntityLookup({
766
+ apiEndpoint,
767
+ value,
768
+ onChange,
769
+ label,
770
+ placeholder,
771
+ mapOption,
772
+ required = false,
773
+ disabled = false,
774
+ error,
775
+ }: EntityLookupProps) {
776
+ const { t } = useTranslation(['common']);
777
+ const [search, setSearch] = useState('');
778
+ const [options, setOptions] = useState<EntityLookupOption[]>([]);
779
+ const [selectedOption, setSelectedOption] = useState<EntityLookupOption | null>(null);
780
+ const [isOpen, setIsOpen] = useState(false);
781
+ const [loading, setLoading] = useState(false);
782
+ const containerRef = useRef<HTMLDivElement>(null);
783
+ const debounceRef = useRef<ReturnType<typeof setTimeout>>();
784
+
785
+ // Load selected entity display on mount (when value is set but no label)
786
+ useEffect(() => {
787
+ if (value && !selectedOption) {
788
+ apiClient.get(`${apiEndpoint}/${value}`)
789
+ .then(res => setSelectedOption(mapOption(res.data)))
790
+ .catch(() => { /* Entity not found — clear */ });
791
+ }
792
+ }, [value, apiEndpoint, mapOption, selectedOption]);
793
+
794
+ // Debounced search — 300ms delay, minimum 2 characters
795
+ const handleSearch = useCallback((term: string) => {
796
+ setSearch(term);
797
+ if (debounceRef.current) clearTimeout(debounceRef.current);
798
+
799
+ if (term.length < 2) {
800
+ setOptions([]);
801
+ return;
802
+ }
803
+
804
+ debounceRef.current = setTimeout(async () => {
805
+ setLoading(true);
806
+ try {
807
+ const res = await apiClient.get(apiEndpoint, {
808
+ params: { search: term, pageSize: 20 },
809
+ });
810
+ setOptions((res.data.items || res.data).map(mapOption));
811
+ } catch {
812
+ setOptions([]);
813
+ } finally {
814
+ setLoading(false);
815
+ }
816
+ }, 300);
817
+ }, [apiEndpoint, mapOption]);
818
+
819
+ // Load initial options when dropdown opens (show first 20)
820
+ const handleOpen = useCallback(async () => {
821
+ if (disabled) return;
822
+ setIsOpen(true);
823
+ if (options.length === 0 && search.length < 2) {
824
+ setLoading(true);
825
+ try {
826
+ const res = await apiClient.get(apiEndpoint, {
827
+ params: { pageSize: 20 },
828
+ });
829
+ setOptions((res.data.items || res.data).map(mapOption));
830
+ } catch {
831
+ setOptions([]);
832
+ } finally {
833
+ setLoading(false);
834
+ }
835
+ }
836
+ }, [disabled, apiEndpoint, mapOption, options.length, search.length]);
837
+
838
+ // Select entity
839
+ const handleSelect = useCallback((option: EntityLookupOption) => {
840
+ setSelectedOption(option);
841
+ onChange(option.id);
842
+ setIsOpen(false);
843
+ setSearch('');
844
+ }, [onChange]);
845
+
846
+ // Clear selection
847
+ const handleClear = useCallback(() => {
848
+ setSelectedOption(null);
849
+ onChange(null);
850
+ setSearch('');
851
+ }, [onChange]);
852
+
853
+ // Close on outside click
854
+ useEffect(() => {
855
+ const handleClickOutside = (e: MouseEvent) => {
856
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
857
+ setIsOpen(false);
858
+ }
859
+ };
860
+ document.addEventListener('mousedown', handleClickOutside);
861
+ return () => document.removeEventListener('mousedown', handleClickOutside);
862
+ }, []);
863
+
864
+ return (
865
+ <div ref={containerRef} className="relative">
866
+ <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
867
+ {label} {required && <span className="text-[var(--error-text)]">*</span>}
868
+ </label>
869
+
870
+ {/* Selected value display OR search input */}
871
+ {selectedOption && !isOpen ? (
872
+ <div className="flex items-center gap-2 px-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)]">
873
+ <div className="flex-1">
874
+ <span className="text-[var(--text-primary)]">{selectedOption.label}</span>
875
+ {selectedOption.sublabel && (
876
+ <span className="ml-2 text-sm text-[var(--text-secondary)]">{selectedOption.sublabel}</span>
877
+ )}
878
+ </div>
879
+ {!disabled && (
880
+ <button type="button" onClick={handleClear} className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]">
881
+ <X className="w-4 h-4" />
882
+ </button>
883
+ )}
884
+ <button type="button" onClick={handleOpen} className="text-[var(--text-secondary)]">
885
+ <ChevronDown className="w-4 h-4" />
886
+ </button>
887
+ </div>
888
+ ) : (
889
+ <div className="relative">
890
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
891
+ <input
892
+ type="text"
893
+ value={search}
894
+ onChange={(e) => handleSearch(e.target.value)}
895
+ onFocus={handleOpen}
896
+ placeholder={placeholder || t('common:actions.search', 'Search...')}
897
+ disabled={disabled}
898
+ className="w-full pl-9 pr-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)] text-[var(--text-primary)] placeholder:text-[var(--text-secondary)] focus:ring-2 focus:ring-[var(--color-accent-500)] focus:border-transparent"
899
+ />
900
+ </div>
901
+ )}
902
+
903
+ {/* Dropdown */}
904
+ {isOpen && (
905
+ <div className="absolute z-50 w-full mt-1 bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[var(--radius-card)] shadow-lg max-h-60 overflow-auto">
906
+ {loading ? (
907
+ <div className="p-3 text-center text-[var(--text-secondary)]">
908
+ {t('common:actions.loading', 'Loading...')}
909
+ </div>
910
+ ) : options.length === 0 ? (
911
+ <div className="p-3 text-center text-[var(--text-secondary)]">
912
+ {search.length < 2
913
+ ? t('common:actions.typeToSearch', 'Type at least 2 characters to search...')
914
+ : t('common:empty.noResults', 'No results found')}
915
+ </div>
916
+ ) : (
917
+ options.map((option) => (
918
+ <button
919
+ key={option.id}
920
+ type="button"
921
+ onClick={() => handleSelect(option)}
922
+ className="w-full px-3 py-2 text-left hover:bg-[var(--bg-hover)] transition-colors"
923
+ >
924
+ <div className="text-[var(--text-primary)]">{option.label}</div>
925
+ {option.sublabel && (
926
+ <div className="text-sm text-[var(--text-secondary)]">{option.sublabel}</div>
927
+ )}
928
+ </button>
929
+ ))
930
+ )}
931
+ </div>
932
+ )}
933
+
934
+ {/* Error message */}
935
+ {error && (
936
+ <p className="mt-1 text-sm text-[var(--error-text)]">{error}</p>
937
+ )}
938
+ </div>
939
+ );
940
+ }
941
+ ```
942
+
943
+ ### Usage in Form Pages
944
+
945
+ ```tsx
946
+ // In EntityCreatePage.tsx or EntityEditPage.tsx
947
+ import { EntityLookup } from '@/components/ui/EntityLookup';
948
+
949
+ // Inside the form:
950
+ <EntityLookup
951
+ apiEndpoint="/api/human-resources/employees"
952
+ value={formData.employeeId}
953
+ onChange={(id) => handleChange('employeeId', id)}
954
+ label={t('module:form.employee', 'Employee')}
955
+ placeholder={t('module:form.employeePlaceholder', 'Search for an employee...')}
956
+ mapOption={(emp) => ({
957
+ id: emp.id,
958
+ label: `${emp.firstName} ${emp.lastName}`,
959
+ sublabel: emp.department || emp.code,
960
+ })}
961
+ required
962
+ error={errors.employeeId}
963
+ />
964
+
965
+ // For DepartmentId FK:
966
+ <EntityLookup
967
+ apiEndpoint="/api/human-resources/departments"
968
+ value={formData.departmentId}
969
+ onChange={(id) => handleChange('departmentId', id)}
970
+ label={t('module:form.department', 'Department')}
971
+ placeholder={t('module:form.departmentPlaceholder', 'Search for a department...')}
972
+ mapOption={(dept) => ({
973
+ id: dept.id,
974
+ label: dept.name,
975
+ sublabel: dept.code,
976
+ })}
977
+ required
978
+ />
979
+ ```
980
+
981
+ ### API Search Endpoint Convention (Backend)
982
+
983
+ For EntityLookup to work, each entity's API MUST support search via query parameter:
984
+
985
+ ```
986
+ GET /api/{resource}?search={term}&pageSize=20
987
+ ```
988
+
989
+ Response format:
990
+ ```json
991
+ {
992
+ "items": [
993
+ { "id": "guid", "code": "EMP001", "name": "John Doe", ... }
994
+ ],
995
+ "totalCount": 42
996
+ }
997
+ ```
998
+
999
+ The backend service's `GetAllAsync` method should accept search parameters:
1000
+
1001
+ ```csharp
1002
+ public async Task<PaginatedResult<EntityResponseDto>> GetAllAsync(
1003
+ string? search = null,
1004
+ int page = 1,
1005
+ int pageSize = 20,
1006
+ CancellationToken ct = default)
1007
+ {
1008
+ var query = _db.Entities
1009
+ .Where(x => x.TenantId == _currentUser.TenantId);
1010
+
1011
+ if (!string.IsNullOrWhiteSpace(search))
1012
+ {
1013
+ query = query.Where(x =>
1014
+ x.Name.Contains(search) ||
1015
+ x.Code.Contains(search));
1016
+ }
1017
+
1018
+ var totalCount = await query.CountAsync(ct);
1019
+ var items = await query
1020
+ .OrderBy(x => x.Name)
1021
+ .Skip((page - 1) * pageSize)
1022
+ .Take(pageSize)
1023
+ .Select(x => new EntityResponseDto { ... })
1024
+ .ToListAsync(ct);
1025
+
1026
+ return new PaginatedResult<EntityResponseDto>(items, totalCount, page, pageSize);
1027
+ }
1028
+ ```
1029
+
1030
+ ### Rules
1031
+
1032
+ - **NEVER** render a `Guid` FK field as `<input type="text">` — always use `EntityLookup`
1033
+ - **NEVER** render a `Guid` FK field as `<select>` — even with API-loaded `<option>` elements, `<select>` is NOT acceptable
1034
+ - **NEVER** ask the user to manually type or paste a GUID/ID
1035
+ - **ALWAYS** provide a search-based selection via `<EntityLookup />` for FK fields
1036
+ - **ALWAYS** show the entity's display name (Name, FullName, Code+Name) not the GUID
1037
+ - **ALWAYS** include `mapOption` to define how the related entity is displayed
1038
+ - **ALWAYS** load the selected entity's display name on mount (for edit forms)
1039
+ - **ALWAYS** support clearing the selection (unless required + already set)
1040
+
1041
+ **Why `<select>` is NOT acceptable for FK fields:**
1042
+ - `<select>` loads ALL options at once — fails with 100+ entities (performance + UX)
1043
+ - `<select>` has no search/filter — user must scroll through all options
1044
+ - `<select>` cannot show sublabels (code, department, etc.)
1045
+ - `EntityLookup` provides: debounced API search, paginated results, display name resolution, sublabels
1046
+
1047
+ **FORBIDDEN:**
1048
+ ```tsx
1049
+ // WRONG: Plain text input for FK field
1050
+ <input
1051
+ type="text"
1052
+ value={formData.employeeId}
1053
+ onChange={(e) => handleChange('employeeId', e.target.value)}
1054
+ placeholder="Enter Employee ID..."
1055
+ />
1056
+
1057
+ // WRONG: <select> dropdown for FK field (even with API-loaded options)
1058
+ <select
1059
+ value={formData.departmentId}
1060
+ onChange={(e) => setFormData({ ...formData, departmentId: e.target.value })}
1061
+ >
1062
+ <option value="">Select Department...</option>
1063
+ {departments.map((dept) => (
1064
+ <option key={dept.id} value={dept.id}>{dept.name}</option>
1065
+ ))}
1066
+ </select>
1067
+
1068
+ // WRONG: Raw GUID displayed to user
1069
+ <span>{entity.departmentId}</span>
1070
+
1071
+ // WRONG: Select with hardcoded options for FK
1072
+ <select onChange={(e) => handleChange('departmentId', e.target.value)}>
1073
+ <option value="guid-1">Department A</option>
1074
+ </select>
1075
+ ```
1076
+
1077
+ **CORRECT — ONLY this pattern:**
1078
+ ```tsx
1079
+ <EntityLookup
1080
+ apiEndpoint="/api/human-resources/departments"
1081
+ value={formData.departmentId}
1082
+ onChange={(id) => handleChange('departmentId', id)}
1083
+ label={t('module:form.department', 'Department')}
1084
+ mapOption={(dept) => ({ id: dept.id, label: dept.name, sublabel: dept.code })}
1085
+ required
1086
+ />
1087
+ ```
1088
+
1089
+ ### I18n Keys for EntityLookup
1090
+
1091
+ Add these keys to the module's translation files:
1092
+
1093
+ ```json
1094
+ {
1095
+ "form": {
1096
+ "employee": "Employee",
1097
+ "employeePlaceholder": "Search for an employee...",
1098
+ "department": "Department",
1099
+ "departmentPlaceholder": "Search for a department..."
1100
+ }
1101
+ }
1102
+ ```
1103
+
1104
+ ---
1105
+
1106
+ ## 7. Documentation Panel Integration (DocToggleButton)
1107
+
1108
+ > **EVERY list/detail page MUST include a `DocToggleButton` in its header.**
1109
+ > This button opens the right-side documentation panel showing the module's user documentation.
1110
+
1111
+ ### Component Import
1112
+
1113
+ ```tsx
1114
+ import { DocToggleButton } from '@/components/docs/DocToggleButton';
1115
+ ```
1116
+
1117
+ ### Placement — Always in the page header actions area (top right)
1118
+
1119
+ ```tsx
1120
+ {/* Header with DocToggleButton */}
1121
+ <div className="flex items-center justify-between">
1122
+ <h1 className="text-2xl font-bold text-[var(--text-primary)]">
1123
+ {t('{module}:title', 'Module Title')}
1124
+ </h1>
1125
+ <div className="flex items-center gap-2">
1126
+ <DocToggleButton />
1127
+ <button onClick={() => navigate('create')} className="...">
1128
+ {t('{module}:actions.create', 'Create')}
1129
+ </button>
1130
+ </div>
1131
+ </div>
1132
+ ```
1133
+
1134
+ ### How it Works
1135
+
1136
+ 1. `DocToggleButton` uses `useDocPanel()` context (provided by the Layout)
1137
+ 2. On click → opens the `DocPanel` on the right side of the screen
1138
+ 3. The panel loads the module's documentation via iframe (`?embedded=true`)
1139
+ 4. Route → doc mapping is in `DocPanelContext.tsx` — maps current pathname to doc URL
1140
+ 5. Panel is resizable (20-60% width), size persists in localStorage
1141
+
1142
+ ### Documentation Generation
1143
+
1144
+ After frontend pages are created, invoke the `/documentation` skill to generate:
1145
+
1146
+ | File | Content |
1147
+ |------|---------|
1148
+ | `src/pages/docs/business/{app}/{module}/doc-data.ts` | Data-driven documentation (~50-80 lines) |
1149
+ | `src/pages/docs/business/{app}/{module}/index.tsx` | Page wrapper (~10 lines) using `DocRenderer` |
1150
+ | `src/i18n/locales/fr/docs-{app}-{module}.json` | French doc translations (source language) |
1151
+
1152
+ The `DocRenderer` shared component renders all 8 documentation sections (overview, use cases, benefits, features, steps, FAQ, business rules, permissions, API endpoints) from the `doc-data.ts` file.
1153
+
1154
+ ### Custom Doc URL (optional)
1155
+
1156
+ If the automatic route mapping doesn't work for your module, pass a custom URL:
1157
+
1158
+ ```tsx
1159
+ <DocToggleButton customDocUrl="/docs/human-resources/employees" />
1160
+ ```
1161
+
1162
+ ### Rules
1163
+
1164
+ - **EVERY** list page MUST include `DocToggleButton` in its header actions
1165
+ - **EVERY** detail page MUST include `DocToggleButton` in its header actions
1166
+ - Create/Edit form pages do NOT need DocToggleButton (users don't read docs while filling forms)
1167
+ - DocToggleButton is imported from `@/components/docs/DocToggleButton` (shared component)
1168
+ - The Layout already provides `DocPanelProvider` — no additional wrapping needed
1169
+ - Documentation content is generated by the `/documentation` skill AFTER frontend pages exist
1170
+
1171
+ ---
1172
+
1173
+ ## 7b. Checklist for /apex Frontend Execution
1174
+
1175
+ Before marking frontend tasks as complete, verify:
1176
+
1177
+ - [ ] All page imports use `React.lazy()` with named export wrapping
1178
+ - [ ] `<Suspense fallback={<PageLoader />}>` wraps all lazy components in routes
1179
+ - [ ] Translation files exist for **all 4 languages** (fr, en, it, de) in `src/i18n/locales/`
1180
+ - [ ] All `t()` calls include namespace prefix AND fallback value
1181
+ - [ ] No hardcoded strings in JSX — all text goes through `t()`
1182
+ - [ ] CSS uses variables only — no hardcoded Tailwind colors (BLOCKING POST-CHECK 13)
1183
+ - [ ] Pages follow loading → error → content pattern
1184
+ - [ ] Pages use `src/pages/{App}/{Module}/` hierarchy
1185
+ - [ ] API calls use generated hooks or `apiClient` (never raw axios)
1186
+ - [ ] Components use SmartTable/SmartFilter/EntityCard (never raw HTML tables)
1187
+ - [ ] **FK fields use `EntityLookup` — ZERO plain text inputs for Guid FK fields**
1188
+ - [ ] **All FK fields have `mapOption` showing display name, not GUID**
1189
+ - [ ] **Backend APIs support `?search=` query parameter for EntityLookup**
1190
+ - [ ] **Create/Edit forms are full pages with own routes — ZERO modals/popups/drawers**
1191
+ - [ ] `EntityCreatePage.tsx` exists with route `/{module}/create`
1192
+ - [ ] `EntityEditPage.tsx` exists with route `/{module}/:id/edit`
1193
+ - [ ] No `<Modal>`, `<Dialog>`, `<Drawer>` imports in form-related pages
1194
+ - [ ] Form pages include back button with `navigate(-1)`
1195
+ - [ ] Form pages are covered by frontend tests (see section 8)
1196
+ - [ ] **`DocToggleButton` present in header of every list/detail page (see section 7)**
1197
+ - [ ] **`/documentation` skill invoked to generate module doc-data.ts**
1198
+
1199
+ ---
1200
+
1201
+ ## 7c. Cross-Tenant Entity UI Patterns
1202
+
1203
+ > **For optional and scoped tenant entities, the frontend MUST provide UI controls to set the scope/visibility.**
1204
+
1205
+ ### Scope Types
1206
+
1207
+ | Type | Behavior | Use case |
1208
+ |------|----------|----------|
1209
+ | **Optional** | Entity can be tenant-specific OR shared (binary choice) | Data that can belong to one org or all orgs |
1210
+ | **Scoped** | Entity has explicit scope enum: Tenant / Shared / Platform | Data with multiple visibility levels |
1211
+
1212
+ ### Scope Selector in Create Forms (Optional Entities)
1213
+
1214
+ For `optional` tenant entities, add a toggle in the create form allowing the user to decide:
1215
+
1216
+ ```tsx
1217
+ import { useState } from 'react';
1218
+ import { useTranslation } from 'react-i18next';
1219
+
1220
+ export function EntityCreatePage() {
1221
+ const { t } = useTranslation(['{module}']);
1222
+ const [formData, setFormData] = useState({
1223
+ name: '',
1224
+ isShared: false, // User decision: tenant-specific (false) or shared (true)
1225
+ });
1226
+
1227
+ const handleScopeChange = (value: string) => {
1228
+ setFormData({ ...formData, isShared: value === 'shared' });
1229
+ };
1230
+
1231
+ return (
1232
+ <div className="space-y-6">
1233
+ {/* ... form header ... */}
1234
+
1235
+ <SmartForm fields={[
1236
+ {
1237
+ name: 'name',
1238
+ type: 'text',
1239
+ label: t('{module}:form.name', 'Name'),
1240
+ required: true,
1241
+ },
1242
+ // Scope selector — binary toggle for optional entities
1243
+ {
1244
+ name: 'scope',
1245
+ type: 'custom',
1246
+ label: t('common:scope', 'Scope'),
1247
+ render: () => (
1248
+ <div className="space-y-2">
1249
+ <label className="block text-sm font-medium text-[var(--text-primary)]">
1250
+ {t('common:scope', 'Scope')}
1251
+ </label>
1252
+ <select
1253
+ value={formData.isShared ? 'shared' : 'tenant'}
1254
+ onChange={(e) => handleScopeChange(e.target.value)}
1255
+ className="w-full px-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)] text-[var(--text-primary)]"
1256
+ >
1257
+ <option value="tenant">
1258
+ {t('common:scope.tenant', 'My Organization')}
1259
+ </option>
1260
+ <option value="shared">
1261
+ {t('common:scope.shared', 'Shared (All Organizations)')}
1262
+ </option>
1263
+ </select>
1264
+ <p className="text-xs text-[var(--text-secondary)]">
1265
+ {formData.isShared
1266
+ ? t('common:scope.shared.hint', 'This data will be accessible to all organizations')
1267
+ : t('common:scope.tenant.hint', 'This data will only be visible to your organization')}
1268
+ </p>
1269
+ </div>
1270
+ ),
1271
+ },
1272
+ ]} />
1273
+ </div>
1274
+ );
1275
+ }
1276
+ ```
1277
+
1278
+ ### Scope Selector in Create Forms (Scoped Entities)
1279
+
1280
+ For `scoped` entities with explicit enum values (Tenant, Shared, Platform), use a dropdown with all scope options:
1281
+
1282
+ ```tsx
1283
+ export function EntityCreatePage() {
1284
+ const { t } = useTranslation(['{module}']);
1285
+ const [formData, setFormData] = useState({
1286
+ name: '',
1287
+ scope: 'Tenant', // Enum: 'Tenant' | 'Shared' | 'Platform'
1288
+ });
1289
+
1290
+ return (
1291
+ <SmartForm fields={[
1292
+ {
1293
+ name: 'name',
1294
+ type: 'text',
1295
+ label: t('{module}:form.name', 'Name'),
1296
+ required: true,
1297
+ },
1298
+ {
1299
+ name: 'scope',
1300
+ type: 'select',
1301
+ label: t('common:scope', 'Scope'),
1302
+ options: [
1303
+ { value: 'Tenant', label: t('common:scope.tenant', 'My Organization') },
1304
+ { value: 'Shared', label: t('common:scope.shared', 'Shared') },
1305
+ { value: 'Platform', label: t('common:scope.platform', 'Platform (Admin Only)') },
1306
+ ],
1307
+ default: 'Tenant',
1308
+ required: true,
1309
+ help: t('common:scope.help', 'Select the visibility level for this data'),
1310
+ },
1311
+ ]} />
1312
+ );
1313
+ }
1314
+ ```
1315
+
1316
+ ### Scope Indicator in List Views
1317
+
1318
+ Display a visual indicator/badge on each row showing the entity scope:
1319
+
1320
+ ```tsx
1321
+ import { useTranslation } from 'react-i18next';
1322
+
1323
+ // ScopeBadge component for reuse
1324
+ interface ScopeBadgeProps {
1325
+ tenantId?: string | null; // For optional entities: null = shared, value = tenant-specific
1326
+ scope?: string; // For scoped entities: 'Tenant' | 'Shared' | 'Platform'
1327
+ }
1328
+
1329
+ export function ScopeBadge({ tenantId, scope }: ScopeBadgeProps) {
1330
+ const { t } = useTranslation(['common']);
1331
+
1332
+ // Optional entity scope
1333
+ if (tenantId !== undefined) {
1334
+ const isTenant = Boolean(tenantId);
1335
+ return (
1336
+ <span
1337
+ className={`px-2 py-1 rounded-full text-xs font-semibold ${
1338
+ isTenant
1339
+ ? 'bg-[var(--bg-accent-light)] text-[var(--color-accent-600)]'
1340
+ : 'bg-[var(--bg-secondary)] text-[var(--text-secondary)]'
1341
+ }`}
1342
+ >
1343
+ {isTenant
1344
+ ? t('common:scope.tenant', 'Tenant')
1345
+ : t('common:scope.shared', 'Shared')}
1346
+ </span>
1347
+ );
1348
+ }
1349
+
1350
+ // Scoped entity scope
1351
+ if (scope) {
1352
+ const scopeStyles: Record<string, { bg: string; text: string }> = {
1353
+ Tenant: {
1354
+ bg: 'bg-[var(--bg-accent-light)]',
1355
+ text: 'text-[var(--color-accent-600)]',
1356
+ },
1357
+ Shared: {
1358
+ bg: 'bg-[var(--bg-secondary)]',
1359
+ text: 'text-[var(--text-secondary)]',
1360
+ },
1361
+ Platform: {
1362
+ bg: 'bg-[var(--bg-warning-light)]',
1363
+ text: 'text-[var(--color-warning-600)]',
1364
+ },
1365
+ };
1366
+
1367
+ const style = scopeStyles[scope] || scopeStyles.Tenant;
1368
+ const scopeLabel = {
1369
+ Tenant: t('common:scope.tenant', 'Organization'),
1370
+ Shared: t('common:scope.shared', 'Shared'),
1371
+ Platform: t('common:scope.platform', 'Platform'),
1372
+ }[scope] || scope;
1373
+
1374
+ return (
1375
+ <span className={`px-2 py-1 rounded-full text-xs font-semibold ${style.bg} ${style.text}`}>
1376
+ {scopeLabel}
1377
+ </span>
1378
+ );
1379
+ }
1380
+
1381
+ return null;
1382
+ }
1383
+ ```
1384
+
1385
+ ### Using ScopeBadge in SmartTable Columns
1386
+
1387
+ ```tsx
1388
+ // In the list page, add a scope column
1389
+ const columns = [
1390
+ { key: 'name', label: t('{module}:columns.name', 'Name') },
1391
+ { key: 'code', label: t('{module}:columns.code', 'Code') },
1392
+ {
1393
+ key: 'scope',
1394
+ label: t('common:scope', 'Scope'),
1395
+ render: (row) => (
1396
+ // For optional entities: show based on tenantId
1397
+ <ScopeBadge tenantId={row.tenantId} />
1398
+ // OR for scoped entities: show based on scope field
1399
+ // <ScopeBadge scope={row.scope} />
1400
+ ),
1401
+ },
1402
+ { key: 'actions', label: t('{module}:columns.actions', 'Actions') },
1403
+ ];
1404
+
1405
+ return (
1406
+ <SmartTable
1407
+ columns={columns}
1408
+ data={data}
1409
+ loading={loading}
1410
+ onRowClick={(row) => navigate(`${row.id}`)}
1411
+ />
1412
+ );
1413
+ ```
1414
+
1415
+ ### I18n Keys for Scope UI
1416
+
1417
+ Add these keys to `src/i18n/locales/*/common.json`:
1418
+
1419
+ ```json
1420
+ {
1421
+ "scope": "Scope",
1422
+ "scope.tenant": "My Organization",
1423
+ "scope.tenant.hint": "This data will only be visible to your organization",
1424
+ "scope.shared": "Shared (All Organizations)",
1425
+ "scope.shared.hint": "This data will be accessible to all organizations",
1426
+ "scope.platform": "Platform (Admin Only)",
1427
+ "scope.help": "Select the visibility level for this data"
1428
+ }
1429
+ ```
1430
+
1431
+ And in the module-specific translation files (e.g., `employees.json`):
1432
+
1433
+ ```json
1434
+ {
1435
+ "form": {
1436
+ "scope": "Scope",
1437
+ "scopeHint": "Choose who can see this data"
1438
+ }
1439
+ }
1440
+ ```
1441
+
1442
+ ### Rules
1443
+
1444
+ - **ALWAYS** provide scope controls in create forms for optional/scoped entities
1445
+ - **ALWAYS** show scope indicator badges in list views
1446
+ - **ALWAYS** use `ScopeBadge` component for consistency across modules
1447
+ - **NEVER** let users create shared entities without explicit choice
1448
+ - **NEVER** hide scope controls — scope is a business-critical property
1449
+ - **ALWAYS** include scope-related translation keys in i18n files (all 4 languages)
1450
+ - **FORBIDDEN:** Form field for scope labeled ambiguously (e.g., "Public/Private" without context)
1451
+ - **FORBIDDEN:** Scope badges with hardcoded colors — always use CSS variables
1452
+
1453
+ ---
1454
+
1455
+ ## 8. Frontend Form Testing
1456
+
1457
+ > **ALL form pages MUST have tests.** Forms are critical user interaction points and MUST be verified.
1458
+
1459
+ ### Required Test Coverage per Form Page
1460
+
1461
+ | Test category | What to verify | Tool |
1462
+ |---------------|---------------|------|
1463
+ | Rendering | Form renders with all expected fields | Vitest + React Testing Library |
1464
+ | Validation | Required fields show errors on empty submit | Vitest + React Testing Library |
1465
+ | Submission | Successful submit calls API and navigates back | Vitest + MSW (mock API) |
1466
+ | Pre-fill (edit) | Edit form loads entity data into fields | Vitest + React Testing Library |
1467
+ | Navigation | Back button calls `navigate(-1)` | Vitest + React Testing Library |
1468
+ | Error handling | API error displays error message | Vitest + MSW |
1469
+
1470
+ ### Test File Convention
1471
+
1472
+ ```
1473
+ src/pages/{App}/{Module}/
1474
+ ├── EntityCreatePage.tsx
1475
+ ├── EntityCreatePage.test.tsx ← MANDATORY
1476
+ ├── EntityEditPage.tsx
1477
+ ├── EntityEditPage.test.tsx ← MANDATORY
1478
+ ├── EntityListPage.tsx
1479
+ └── EntityDetailPage.tsx
1480
+ ```
1481
+
1482
+ ### Create Page Test Template
1483
+
1484
+ ```tsx
1485
+ import { render, screen, waitFor } from '@testing-library/react';
1486
+ import userEvent from '@testing-library/user-event';
1487
+ import { MemoryRouter } from 'react-router-dom';
1488
+ import { describe, it, expect, vi } from 'vitest';
1489
+ import { EntityCreatePage } from './EntityCreatePage';
1490
+
1491
+ // Mock API
1492
+ vi.mock('@/services/api/apiClient');
1493
+ const mockNavigate = vi.fn();
1494
+ vi.mock('react-router-dom', async () => ({
1495
+ ...(await vi.importActual('react-router-dom')),
1496
+ useNavigate: () => mockNavigate,
1497
+ }));
1498
+
1499
+ describe('EntityCreatePage', () => {
1500
+ it('renders the create form with all fields', () => {
1501
+ render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
1502
+ expect(screen.getByRole('textbox', { name: /name/i })).toBeInTheDocument();
1503
+ // Verify all expected form fields
1504
+ });
1505
+
1506
+ it('shows validation errors on empty submit', async () => {
1507
+ render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
1508
+ await userEvent.click(screen.getByRole('button', { name: /save|create/i }));
1509
+ await waitFor(() => {
1510
+ expect(screen.getByText(/required/i)).toBeInTheDocument();
1511
+ });
1512
+ });
1513
+
1514
+ it('submits form and navigates back on success', async () => {
1515
+ render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
1516
+ await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'Test');
1517
+ await userEvent.click(screen.getByRole('button', { name: /save|create/i }));
1518
+ await waitFor(() => {
1519
+ expect(mockNavigate).toHaveBeenCalledWith(-1);
1520
+ });
1521
+ });
1522
+
1523
+ it('navigates back on cancel/back button', async () => {
1524
+ render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
1525
+ await userEvent.click(screen.getByRole('button', { name: /back|cancel/i }));
1526
+ expect(mockNavigate).toHaveBeenCalledWith(-1);
1527
+ });
1528
+ });
1529
+ ```
1530
+
1531
+ ### Edit Page Test Template
1532
+
1533
+ ```tsx
1534
+ describe('EntityEditPage', () => {
1535
+ it('loads entity data and pre-fills the form', async () => {
1536
+ render(<MemoryRouter initialEntries={['/entities/123/edit']}><EntityEditPage /></MemoryRouter>);
1537
+ await waitFor(() => {
1538
+ expect(screen.getByDisplayValue('Existing Name')).toBeInTheDocument();
1539
+ });
1540
+ });
1541
+
1542
+ it('submits updated data and navigates back', async () => {
1543
+ render(<MemoryRouter initialEntries={['/entities/123/edit']}><EntityEditPage /></MemoryRouter>);
1544
+ await waitFor(() => screen.getByDisplayValue('Existing Name'));
1545
+ await userEvent.clear(screen.getByRole('textbox', { name: /name/i }));
1546
+ await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'Updated');
1547
+ await userEvent.click(screen.getByRole('button', { name: /save/i }));
1548
+ await waitFor(() => {
1549
+ expect(mockNavigate).toHaveBeenCalledWith(-1);
1550
+ });
1551
+ });
1552
+
1553
+ it('displays error when API call fails', async () => {
1554
+ // Mock API to reject
1555
+ render(<MemoryRouter initialEntries={['/entities/123/edit']}><EntityEditPage /></MemoryRouter>);
1556
+ // ... trigger submit with mocked failure
1557
+ await waitFor(() => {
1558
+ expect(screen.getByText(/failed/i)).toBeInTheDocument();
1559
+ });
1560
+ });
1561
+ });
1562
+ ```
1563
+
1564
+ ### Rules
1565
+
1566
+ - **EVERY** `EntityCreatePage.tsx` MUST have a companion `EntityCreatePage.test.tsx`
1567
+ - **EVERY** `EntityEditPage.tsx` MUST have a companion `EntityEditPage.test.tsx`
1568
+ - Tests MUST cover: rendering, validation, submit success, submit error, navigation
1569
+ - Use `@testing-library/react` + `@testing-library/user-event` (NEVER enzyme)
1570
+ - Mock API with `vi.mock()` or MSW — NEVER make real API calls in tests
1571
+ - Test files live next to their component (co-located, NOT in a separate `__tests__/` folder)