@atlashub/smartstack-cli 3.39.0 → 3.41.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 (472) 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 +6 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp-entry.mjs +6 -4
  14. package/dist/mcp-entry.mjs.map +1 -1
  15. package/package.json +115 -115
  16. package/scripts/extract-api-endpoints.ts +325 -325
  17. package/scripts/extract-business-rules.ts +440 -440
  18. package/scripts/generate-doc-with-mock-ui.ts +804 -804
  19. package/scripts/health-check.sh +168 -168
  20. package/scripts/postinstall.js +18 -18
  21. package/templates/agents/action.md +37 -37
  22. package/templates/agents/ba-reader.md +378 -378
  23. package/templates/agents/ba-writer.md +861 -861
  24. package/templates/agents/code-reviewer.md +163 -163
  25. package/templates/agents/db-reader.md +149 -149
  26. package/templates/agents/docs-context-reader.md +143 -143
  27. package/templates/agents/docs-sync-checker.md +122 -122
  28. package/templates/agents/efcore/conflicts.md +95 -84
  29. package/templates/agents/efcore/db-deploy.md +85 -74
  30. package/templates/agents/efcore/db-reset.md +96 -85
  31. package/templates/agents/efcore/db-seed.md +72 -61
  32. package/templates/agents/efcore/db-status.md +97 -86
  33. package/templates/agents/efcore/migration.md +197 -186
  34. package/templates/agents/efcore/rebase-snapshot.md +119 -108
  35. package/templates/agents/efcore/scan.md +103 -92
  36. package/templates/agents/efcore/squash.md +172 -161
  37. package/templates/agents/explore-codebase.md +66 -66
  38. package/templates/agents/explore-docs.md +98 -98
  39. package/templates/agents/fix-grammar.md +50 -50
  40. package/templates/agents/gitflow/abort.md +45 -45
  41. package/templates/agents/gitflow/cleanup.md +96 -96
  42. package/templates/agents/gitflow/commit.md +236 -236
  43. package/templates/agents/gitflow/exec.md +48 -48
  44. package/templates/agents/gitflow/finish.md +146 -146
  45. package/templates/agents/gitflow/init-clone.md +199 -199
  46. package/templates/agents/gitflow/init-detect.md +137 -137
  47. package/templates/agents/gitflow/init-validate.md +225 -225
  48. package/templates/agents/gitflow/init.md +340 -340
  49. package/templates/agents/gitflow/merge.md +145 -145
  50. package/templates/agents/gitflow/plan.md +42 -42
  51. package/templates/agents/gitflow/pr.md +191 -191
  52. package/templates/agents/gitflow/review.md +49 -49
  53. package/templates/agents/gitflow/start.md +147 -147
  54. package/templates/agents/gitflow/status.md +95 -95
  55. package/templates/agents/mcp-healthcheck.md +163 -163
  56. package/templates/agents/snipper.md +37 -37
  57. package/templates/agents/websearch.md +46 -46
  58. package/templates/hooks/appsettings-guard.sh +76 -76
  59. package/templates/hooks/docs-drift-check.md +96 -96
  60. package/templates/hooks/ef-migration-check.md +139 -139
  61. package/templates/hooks/hooks.json +58 -58
  62. package/templates/hooks/mcp-check.md +64 -64
  63. package/templates/hooks/ralph-mcp-logger.sh +46 -46
  64. package/templates/hooks/ralph-session-end.sh +69 -69
  65. package/templates/hooks/stop-hook.sh +177 -177
  66. package/templates/hooks/wsl-dotnet-cleanup.sh +24 -24
  67. package/templates/mcp-scaffolding/component.tsx.hbs +318 -318
  68. package/templates/mcp-scaffolding/controller.cs.hbs +192 -192
  69. package/templates/mcp-scaffolding/entity-extension.cs.hbs +239 -239
  70. package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -116
  71. package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -133
  72. package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -126
  73. package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -261
  74. package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -53
  75. package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +436 -436
  76. package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -239
  77. package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -441
  78. package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -442
  79. package/templates/mcp-scaffolding/tests/service.test.cs.hbs +402 -402
  80. package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -428
  81. package/templates/project/DependencyInjection.Application.cs.template +25 -25
  82. package/templates/project/DependencyInjection.Infrastructure.cs.template +61 -61
  83. package/templates/project/DesignTimeExtensionsDbContextFactory.cs.template +70 -70
  84. package/templates/project/ExampleEntity.cs.template +116 -116
  85. package/templates/project/ExampleEntityConfiguration.cs.template +64 -64
  86. package/templates/project/ExampleService.cs.template +146 -146
  87. package/templates/project/ExtensionsDbContext.cs.template +41 -41
  88. package/templates/project/IExtensionsDbContext.cs.template +22 -22
  89. package/templates/project/Program.cs.template +47 -47
  90. package/templates/project/README.md +79 -79
  91. package/templates/project/api.ts.template +12 -12
  92. package/templates/project/appsettings.json.template +170 -170
  93. package/templates/project/claude-settings.json.template +5 -5
  94. package/templates/project/test-frontend/msw/handlers.ts +58 -58
  95. package/templates/project/test-frontend/msw/server.ts +25 -25
  96. package/templates/project/test-frontend/setup.ts +16 -16
  97. package/templates/project/test-frontend/test-utils.tsx +59 -59
  98. package/templates/project/test-frontend/vitest.config.ts +31 -31
  99. package/templates/ralph/README.md +93 -93
  100. package/templates/ralph/ralph.config.yaml +113 -113
  101. package/templates/scripts/setup-ralph-loop.sh +173 -173
  102. package/templates/skills/_resources/config-safety.md +61 -61
  103. package/templates/skills/_resources/context-digest-template.md +53 -53
  104. package/templates/skills/_resources/doc-context-cache.md +60 -60
  105. package/templates/skills/_resources/docs-manifest-schema.md +155 -155
  106. package/templates/skills/_resources/formatting-guide.md +124 -124
  107. package/templates/skills/_resources/mcp-validate-documentation-spec.md +181 -181
  108. package/templates/skills/_shared.md +228 -228
  109. package/templates/skills/admin/SKILL.md +48 -48
  110. package/templates/skills/ai-prompt/SKILL.md +107 -107
  111. package/templates/skills/ai-prompt/steps/step-00-init.md +47 -47
  112. package/templates/skills/ai-prompt/steps/step-01-implementation.md +122 -122
  113. package/templates/skills/apex/SKILL.md +168 -168
  114. package/templates/skills/apex/_shared.md +141 -141
  115. package/templates/skills/apex/references/agent-teams-protocol.md +164 -164
  116. package/templates/skills/apex/references/analysis-methods.md +141 -141
  117. package/templates/skills/apex/references/challenge-questions.md +145 -145
  118. package/templates/skills/apex/references/code-generation.md +412 -412
  119. package/templates/skills/apex/references/core-seed-data.md +1437 -1437
  120. package/templates/skills/apex/references/error-classification.md +144 -144
  121. package/templates/skills/apex/references/examine-build-validation.md +82 -82
  122. package/templates/skills/apex/references/execution-frontend-gates.md +177 -177
  123. package/templates/skills/apex/references/execution-frontend-patterns.md +105 -105
  124. package/templates/skills/apex/references/execution-layer1-rules.md +96 -96
  125. package/templates/skills/apex/references/initialization-challenge-flow.md +110 -110
  126. package/templates/skills/apex/references/planning-layer-mapping.md +151 -151
  127. package/templates/skills/apex/references/post-checks.md +1584 -1584
  128. package/templates/skills/apex/references/smartstack-api.md +1053 -1053
  129. package/templates/skills/apex/references/smartstack-frontend.md +1571 -1571
  130. package/templates/skills/apex/references/smartstack-layers.md +402 -402
  131. package/templates/skills/apex/steps/step-00-init.md +307 -307
  132. package/templates/skills/apex/steps/step-01-analyze.md +165 -165
  133. package/templates/skills/apex/steps/step-02-plan.md +144 -144
  134. package/templates/skills/apex/steps/step-03-execute.md +328 -328
  135. package/templates/skills/apex/steps/step-04-examine.md +263 -263
  136. package/templates/skills/apex/steps/step-05-deep-review.md +129 -129
  137. package/templates/skills/apex/steps/step-06-resolve.md +101 -101
  138. package/templates/skills/apex/steps/step-07-tests.md +238 -238
  139. package/templates/skills/apex/steps/step-08-run-tests.md +125 -125
  140. package/templates/skills/application/SKILL.md +4 -4
  141. package/templates/skills/application/references/application-roles-template.md +227 -227
  142. package/templates/skills/application/references/backend-controller-hierarchy.md +58 -58
  143. package/templates/skills/application/references/backend-entity-seeding.md +72 -72
  144. package/templates/skills/application/references/backend-seeding-and-dto-output.md +83 -83
  145. package/templates/skills/application/references/backend-table-prefix-mapping.md +79 -79
  146. package/templates/skills/application/references/backend-verification.md +88 -88
  147. package/templates/skills/application/references/frontend-i18n-and-output.md +67 -67
  148. package/templates/skills/application/references/frontend-route-naming.md +117 -117
  149. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +107 -107
  150. package/templates/skills/application/references/frontend-verification.md +156 -156
  151. package/templates/skills/application/references/migration-checklist-troubleshooting.md +1 -1
  152. package/templates/skills/application/references/provider-template.md +177 -177
  153. package/templates/skills/application/references/roles-client-project-handling.md +55 -55
  154. package/templates/skills/application/references/roles-fallback-procedure.md +149 -149
  155. package/templates/skills/application/references/test-coverage-requirements.md +213 -213
  156. package/templates/skills/application/references/test-frontend.md +73 -73
  157. package/templates/skills/application/references/test-prerequisites.md +72 -72
  158. package/templates/skills/application/steps/step-05-frontend.md +176 -176
  159. package/templates/skills/application/steps/step-06-migration.md +193 -193
  160. package/templates/skills/application/steps/step-07-tests.md +356 -356
  161. package/templates/skills/application/steps/step-08-documentation.md +137 -137
  162. package/templates/skills/application/templates-backend.md +463 -463
  163. package/templates/skills/application/templates-frontend.md +685 -685
  164. package/templates/skills/application/templates-i18n.md +520 -520
  165. package/templates/skills/application/templates-seed.md +1096 -1096
  166. package/templates/skills/business-analyse/SKILL.md +327 -327
  167. package/templates/skills/business-analyse/_architecture.md +123 -123
  168. package/templates/skills/business-analyse/_elicitation.md +206 -206
  169. package/templates/skills/business-analyse/_module-loop.md +115 -115
  170. package/templates/skills/business-analyse/_shared.md +383 -383
  171. package/templates/skills/business-analyse/_suggestions.md +34 -34
  172. package/templates/skills/business-analyse/html/ba-interactive.html +4477 -4477
  173. package/templates/skills/business-analyse/html/build-html.js +77 -77
  174. package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +150 -150
  175. package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +227 -227
  176. package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +199 -199
  177. package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +205 -205
  178. package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +647 -647
  179. package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +195 -195
  180. package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +92 -92
  181. package/templates/skills/business-analyse/html/src/scripts/08-editing.js +135 -135
  182. package/templates/skills/business-analyse/html/src/scripts/09-export.js +168 -168
  183. package/templates/skills/business-analyse/html/src/scripts/10-comments.js +171 -171
  184. package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +166 -166
  185. package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -38
  186. package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -101
  187. package/templates/skills/business-analyse/html/src/styles/03-navigation.css +120 -120
  188. package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -196
  189. package/templates/skills/business-analyse/html/src/styles/05-modules.css +454 -454
  190. package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +272 -272
  191. package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -184
  192. package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +241 -241
  193. package/templates/skills/business-analyse/html/src/template.html +516 -516
  194. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +546 -546
  195. package/templates/skills/business-analyse/questionnaire/00-application.md +160 -160
  196. package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -85
  197. package/templates/skills/business-analyse/questionnaire/01-context.md +185 -185
  198. package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +189 -189
  199. package/templates/skills/business-analyse/questionnaire/03-scope.md +164 -164
  200. package/templates/skills/business-analyse/questionnaire/04-data.md +88 -88
  201. package/templates/skills/business-analyse/questionnaire/05-integrations.md +58 -58
  202. package/templates/skills/business-analyse/questionnaire/06-security.md +68 -68
  203. package/templates/skills/business-analyse/questionnaire/07-ui.md +76 -76
  204. package/templates/skills/business-analyse/questionnaire/08-performance.md +42 -42
  205. package/templates/skills/business-analyse/questionnaire/09-constraints.md +45 -45
  206. package/templates/skills/business-analyse/questionnaire/10-documentation.md +43 -43
  207. package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +59 -59
  208. package/templates/skills/business-analyse/questionnaire/12-migration.md +58 -58
  209. package/templates/skills/business-analyse/questionnaire/13-cross-module.md +69 -69
  210. package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -135
  211. package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -136
  212. package/templates/skills/business-analyse/questionnaire.md +337 -337
  213. package/templates/skills/business-analyse/react/application-viewer.md +242 -242
  214. package/templates/skills/business-analyse/react/components.md +551 -551
  215. package/templates/skills/business-analyse/react/i18n-template.md +306 -306
  216. package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -169
  217. package/templates/skills/business-analyse/references/agent-module-prompt.md +362 -362
  218. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +557 -557
  219. package/templates/skills/business-analyse/references/analysis-semantic-checks.md +190 -190
  220. package/templates/skills/business-analyse/references/cache-warming-strategy.md +566 -566
  221. package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +41 -41
  222. package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +74 -74
  223. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +115 -115
  224. package/templates/skills/business-analyse/references/cadrage-shared-modules.md +68 -69
  225. package/templates/skills/business-analyse/references/cadrage-structure-cards.md +85 -85
  226. package/templates/skills/business-analyse/references/compilation-structure-cards.md +297 -297
  227. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +107 -107
  228. package/templates/skills/business-analyse/references/deploy-data-build.md +180 -180
  229. package/templates/skills/business-analyse/references/deploy-modes.md +118 -118
  230. package/templates/skills/business-analyse/references/detection-strategies.md +424 -424
  231. package/templates/skills/business-analyse/references/entity-architecture-decision.md +218 -218
  232. package/templates/skills/business-analyse/references/handoff-file-templates.md +120 -120
  233. package/templates/skills/business-analyse/references/handoff-mappings.md +81 -81
  234. package/templates/skills/business-analyse/references/handoff-seeddata-generation.md +312 -312
  235. package/templates/skills/business-analyse/references/html-data-mapping.md +299 -299
  236. package/templates/skills/business-analyse/references/init-schema-deployment.md +65 -65
  237. package/templates/skills/business-analyse/references/naming-conventions.md +243 -243
  238. package/templates/skills/business-analyse/references/prd-generation.md +258 -258
  239. package/templates/skills/business-analyse/references/review-data-mapping.md +363 -363
  240. package/templates/skills/business-analyse/references/robustness-checks.md +542 -542
  241. package/templates/skills/business-analyse/references/spec-auto-inference.md +111 -111
  242. package/templates/skills/business-analyse/references/team-orchestration.md +1022 -1022
  243. package/templates/skills/business-analyse/references/ui-dashboard-spec.md +85 -85
  244. package/templates/skills/business-analyse/references/ui-resource-cards.md +259 -259
  245. package/templates/skills/business-analyse/references/validate-incremental-html.md +121 -121
  246. package/templates/skills/business-analyse/references/validation-checklist.md +347 -347
  247. package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -335
  248. package/templates/skills/business-analyse/schemas/application-schema.json +453 -453
  249. package/templates/skills/business-analyse/schemas/feature-schema.json +53 -53
  250. package/templates/skills/business-analyse/schemas/project-schema.json +485 -485
  251. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +201 -201
  252. package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +82 -82
  253. package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +80 -80
  254. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +70 -70
  255. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +547 -547
  256. package/templates/skills/business-analyse/schemas/sections/validation-schema.json +93 -93
  257. package/templates/skills/business-analyse/schemas/shared/common-defs.json +226 -226
  258. package/templates/skills/business-analyse/steps/step-00-init.md +575 -576
  259. package/templates/skills/business-analyse/steps/step-01-cadrage.md +767 -767
  260. package/templates/skills/business-analyse/steps/step-01b-applications.md +419 -419
  261. package/templates/skills/business-analyse/steps/step-02-decomposition.md +387 -387
  262. package/templates/skills/business-analyse/steps/step-03a-data.md +16 -16
  263. package/templates/skills/business-analyse/steps/step-03a1-setup.md +506 -506
  264. package/templates/skills/business-analyse/steps/step-03a2-analysis.md +252 -252
  265. package/templates/skills/business-analyse/steps/step-03b-ui.md +425 -425
  266. package/templates/skills/business-analyse/steps/step-03c-compile.md +611 -611
  267. package/templates/skills/business-analyse/steps/step-03d-validate.md +783 -783
  268. package/templates/skills/business-analyse/steps/step-04-consolidation.md +17 -17
  269. package/templates/skills/business-analyse/steps/step-04a-collect.md +415 -415
  270. package/templates/skills/business-analyse/steps/step-04b-analyze.md +163 -163
  271. package/templates/skills/business-analyse/steps/step-04c-decide.md +186 -186
  272. package/templates/skills/business-analyse/steps/step-05a-handoff.md +840 -840
  273. package/templates/skills/business-analyse/steps/step-05b-deploy.md +522 -522
  274. package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +703 -703
  275. package/templates/skills/business-analyse/steps/step-06-review.md +278 -278
  276. package/templates/skills/business-analyse/templates/tpl-frd.md +168 -168
  277. package/templates/skills/business-analyse/templates/tpl-handoff.md +186 -186
  278. package/templates/skills/business-analyse/templates/tpl-launch-displays.md +59 -59
  279. package/templates/skills/business-analyse/templates/tpl-progress.md +172 -172
  280. package/templates/skills/business-analyse/templates-frd.md +476 -476
  281. package/templates/skills/business-analyse/templates-react.md +574 -574
  282. package/templates/skills/cc-agent/SKILL.md +129 -129
  283. package/templates/skills/cc-agent/references/agent-behavior-patterns.md +95 -95
  284. package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -213
  285. package/templates/skills/cc-agent/references/permission-modes.md +102 -102
  286. package/templates/skills/cc-agent/references/tools-reference.md +144 -144
  287. package/templates/skills/cc-agent/steps/step-00-init.md +134 -134
  288. package/templates/skills/cc-agent/steps/step-01-design.md +186 -186
  289. package/templates/skills/cc-agent/steps/step-02-generate.md +131 -131
  290. package/templates/skills/cc-agent/steps/step-03-validate.md +130 -130
  291. package/templates/skills/cc-agent/templates/agent-categorized.md +67 -67
  292. package/templates/skills/cc-agent/templates/agent-standalone.md +56 -56
  293. package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -94
  294. package/templates/skills/cc-audit/SKILL.md +108 -108
  295. package/templates/skills/cc-audit/references/agent-checklist.md +91 -91
  296. package/templates/skills/cc-audit/references/hook-checklist.md +110 -110
  297. package/templates/skills/cc-audit/references/skill-checklist.md +70 -70
  298. package/templates/skills/cc-audit/steps/step-00-init.md +98 -98
  299. package/templates/skills/cc-audit/steps/step-01-scan.md +142 -142
  300. package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -158
  301. package/templates/skills/cc-audit/steps/step-03-report.md +142 -142
  302. package/templates/skills/cc-skill/SKILL.md +134 -134
  303. package/templates/skills/cc-skill/references/best-practices.md +167 -167
  304. package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -182
  305. package/templates/skills/cc-skill/references/skill-patterns.md +199 -199
  306. package/templates/skills/cc-skill/steps/step-00-init.md +119 -119
  307. package/templates/skills/cc-skill/steps/step-01-design.md +199 -199
  308. package/templates/skills/cc-skill/steps/step-02-generate.md +145 -145
  309. package/templates/skills/cc-skill/steps/step-03-steps.md +151 -151
  310. package/templates/skills/cc-skill/steps/step-04-validate.md +124 -124
  311. package/templates/skills/cc-skill/templates/skill-forked.md +85 -85
  312. package/templates/skills/cc-skill/templates/skill-progressive.md +102 -102
  313. package/templates/skills/cc-skill/templates/skill-simple.md +75 -75
  314. package/templates/skills/cc-skill/templates/step-template.md +82 -82
  315. package/templates/skills/check-version/SKILL.md +196 -196
  316. package/templates/skills/controller/SKILL.md +162 -162
  317. package/templates/skills/controller/postman-templates.md +614 -614
  318. package/templates/skills/controller/references/controller-code-templates.md +159 -159
  319. package/templates/skills/controller/references/mcp-scaffold-workflow.md +209 -209
  320. package/templates/skills/controller/references/permission-sync-templates.md +149 -149
  321. package/templates/skills/controller/steps/step-00-init.md +193 -191
  322. package/templates/skills/controller/steps/step-01-analyze.md +146 -146
  323. package/templates/skills/controller/steps/step-02-plan.md +176 -176
  324. package/templates/skills/controller/steps/step-03-generate.md +189 -189
  325. package/templates/skills/controller/steps/step-04-perms.md +80 -80
  326. package/templates/skills/controller/steps/step-05-validate.md +107 -107
  327. package/templates/skills/controller/templates.md +1555 -1555
  328. package/templates/skills/debug/SKILL.md +70 -70
  329. package/templates/skills/debug/references/team-protocol.md +232 -232
  330. package/templates/skills/debug/steps/step-00-init.md +57 -57
  331. package/templates/skills/debug/steps/step-01-analyze.md +219 -219
  332. package/templates/skills/debug/steps/step-02-resolve.md +85 -85
  333. package/templates/skills/documentation/SKILL.md +132 -132
  334. package/templates/skills/documentation/data-schema.md +227 -227
  335. package/templates/skills/documentation/steps/step-00-init.md +70 -70
  336. package/templates/skills/documentation/steps/step-01-scan.md +113 -113
  337. package/templates/skills/documentation/steps/step-02-generate.md +231 -231
  338. package/templates/skills/documentation/steps/step-03-validate.md +251 -238
  339. package/templates/skills/documentation/templates.md +662 -663
  340. package/templates/skills/efcore/SKILL.md +168 -167
  341. package/templates/skills/efcore/references/both-contexts.md +32 -32
  342. package/templates/skills/efcore/references/database-operations.md +67 -67
  343. package/templates/skills/efcore/references/destructive-operations.md +38 -38
  344. package/templates/skills/efcore/references/reset-operations.md +81 -81
  345. package/templates/skills/efcore/references/seed-methods.md +86 -86
  346. package/templates/skills/efcore/references/shared-init-functions.md +250 -250
  347. package/templates/skills/efcore/references/sql-objects-injection.md +61 -61
  348. package/templates/skills/efcore/references/troubleshooting.md +81 -81
  349. package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -227
  350. package/templates/skills/efcore/steps/db/step-deploy.md +217 -217
  351. package/templates/skills/efcore/steps/db/step-reset.md +186 -186
  352. package/templates/skills/efcore/steps/db/step-seed.md +166 -166
  353. package/templates/skills/efcore/steps/db/step-status.md +173 -173
  354. package/templates/skills/efcore/steps/migration/step-00-init.md +102 -102
  355. package/templates/skills/efcore/steps/migration/step-01-check.md +164 -164
  356. package/templates/skills/efcore/steps/migration/step-02-create.md +160 -160
  357. package/templates/skills/efcore/steps/migration/step-03-validate.md +168 -168
  358. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +173 -173
  359. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +100 -100
  360. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +115 -115
  361. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +112 -112
  362. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +157 -157
  363. package/templates/skills/efcore/steps/shared/step-00-init.md +131 -131
  364. package/templates/skills/efcore/steps/squash/step-00-init.md +141 -141
  365. package/templates/skills/efcore/steps/squash/step-01-backup.md +120 -120
  366. package/templates/skills/efcore/steps/squash/step-02-fetch.md +168 -168
  367. package/templates/skills/efcore/steps/squash/step-03-create.md +184 -184
  368. package/templates/skills/efcore/steps/squash/step-04-validate.md +174 -174
  369. package/templates/skills/explore/SKILL.md +98 -98
  370. package/templates/skills/feature-full/SKILL.md +111 -111
  371. package/templates/skills/feature-full/steps/step-00-init.md +57 -57
  372. package/templates/skills/feature-full/steps/step-01-implementation.md +120 -120
  373. package/templates/skills/gitflow/SKILL.md +377 -377
  374. package/templates/skills/gitflow/_shared.md +620 -620
  375. package/templates/skills/gitflow/phases/abort.md +189 -189
  376. package/templates/skills/gitflow/phases/cleanup.md +234 -234
  377. package/templates/skills/gitflow/phases/status.md +192 -192
  378. package/templates/skills/gitflow/references/commit-message-generation.md +58 -58
  379. package/templates/skills/gitflow/references/commit-migration-validation.md +49 -49
  380. package/templates/skills/gitflow/references/finish-cleanup.md +55 -55
  381. package/templates/skills/gitflow/references/finish-version-bumping.md +45 -45
  382. package/templates/skills/gitflow/references/init-config-template.md +135 -135
  383. package/templates/skills/gitflow/references/init-environment-detection.md +41 -41
  384. package/templates/skills/gitflow/references/init-name-normalization.md +103 -103
  385. package/templates/skills/gitflow/references/init-questions.md +185 -185
  386. package/templates/skills/gitflow/references/init-structure-creation.md +75 -75
  387. package/templates/skills/gitflow/references/init-version-detection.md +21 -21
  388. package/templates/skills/gitflow/references/init-workspace-detection.md +43 -43
  389. package/templates/skills/gitflow/references/merge-ci-status.md +36 -36
  390. package/templates/skills/gitflow/references/merge-execution.md +62 -62
  391. package/templates/skills/gitflow/references/merge-pr-context.md +76 -76
  392. package/templates/skills/gitflow/references/plan-template.md +69 -69
  393. package/templates/skills/gitflow/references/pr-build-checks.md +60 -60
  394. package/templates/skills/gitflow/references/pr-generation.md +58 -58
  395. package/templates/skills/gitflow/references/start-branch-normalization.md +28 -28
  396. package/templates/skills/gitflow/references/start-efcore-preflight.md +70 -70
  397. package/templates/skills/gitflow/references/start-local-config.md +113 -113
  398. package/templates/skills/gitflow/references/start-worktree-creation.md +50 -50
  399. package/templates/skills/gitflow/references/sync-push-verify.md +44 -44
  400. package/templates/skills/gitflow/references/sync-rebase-conflicts.md +38 -38
  401. package/templates/skills/gitflow/steps/step-commit.md +199 -199
  402. package/templates/skills/gitflow/steps/step-finish.md +147 -147
  403. package/templates/skills/gitflow/steps/step-init.md +190 -190
  404. package/templates/skills/gitflow/steps/step-merge.md +85 -85
  405. package/templates/skills/gitflow/steps/step-plan.md +151 -151
  406. package/templates/skills/gitflow/steps/step-pr.md +199 -199
  407. package/templates/skills/gitflow/steps/step-start.md +195 -195
  408. package/templates/skills/gitflow/steps/step-sync.md +161 -161
  409. package/templates/skills/gitflow/templates/config.json +72 -72
  410. package/templates/skills/mcp/SKILL.md +62 -62
  411. package/templates/skills/mcp/steps/step-01-healthcheck.md +108 -108
  412. package/templates/skills/mcp/steps/step-02-tools.md +73 -73
  413. package/templates/skills/notification/SKILL.md +173 -173
  414. package/templates/skills/quick-search/SKILL.md +99 -99
  415. package/templates/skills/ralph-loop/SKILL.md +234 -234
  416. package/templates/skills/ralph-loop/references/category-completeness.md +185 -185
  417. package/templates/skills/ralph-loop/references/category-rules.md +96 -96
  418. package/templates/skills/ralph-loop/references/compact-loop.md +300 -300
  419. package/templates/skills/ralph-loop/references/init-resume-recovery.md +127 -127
  420. package/templates/skills/ralph-loop/references/module-transition.md +151 -151
  421. package/templates/skills/ralph-loop/references/multi-module-queue.md +171 -171
  422. package/templates/skills/ralph-loop/references/parallel-execution.md +246 -246
  423. package/templates/skills/ralph-loop/references/section-splitting.md +439 -439
  424. package/templates/skills/ralph-loop/references/task-transform-legacy.md +256 -256
  425. package/templates/skills/ralph-loop/references/team-orchestration.md +547 -547
  426. package/templates/skills/ralph-loop/steps/step-00-init.md +150 -150
  427. package/templates/skills/ralph-loop/steps/step-01-task.md +174 -174
  428. package/templates/skills/ralph-loop/steps/step-02-execute.md +177 -177
  429. package/templates/skills/ralph-loop/steps/step-03-commit.md +92 -92
  430. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -207
  431. package/templates/skills/ralph-loop/steps/step-05-report.md +175 -175
  432. package/templates/skills/refactor/SKILL.md +56 -56
  433. package/templates/skills/refactor/steps/step-01-discover.md +60 -60
  434. package/templates/skills/refactor/steps/step-02-execute.md +67 -67
  435. package/templates/skills/review-code/SKILL.md +95 -94
  436. package/templates/skills/review-code/references/clean-code-principles.md +292 -292
  437. package/templates/skills/review-code/references/code-quality-metrics.md +174 -174
  438. package/templates/skills/review-code/references/feedback-patterns.md +149 -149
  439. package/templates/skills/review-code/references/owasp-api-top10.md +243 -243
  440. package/templates/skills/review-code/references/security-checklist.md +212 -212
  441. package/templates/skills/review-code/steps/step-01-smartstack.md +96 -96
  442. package/templates/skills/review-code/steps/step-02-detailed-review.md +80 -80
  443. package/templates/skills/review-code/steps/step-03-react.md +44 -44
  444. package/templates/skills/ui-components/SKILL.md +137 -137
  445. package/templates/skills/ui-components/accessibility.md +170 -170
  446. package/templates/skills/ui-components/patterns/dashboard-chart.md +327 -327
  447. package/templates/skills/ui-components/patterns/data-table.md +39 -39
  448. package/templates/skills/ui-components/patterns/entity-card.md +77 -77
  449. package/templates/skills/ui-components/patterns/grid-layout.md +91 -91
  450. package/templates/skills/ui-components/patterns/kanban.md +43 -43
  451. package/templates/skills/ui-components/responsive-guidelines.md +278 -278
  452. package/templates/skills/ui-components/style-guide.md +113 -113
  453. package/templates/skills/utils/SKILL.md +44 -44
  454. package/templates/skills/utils/subcommands/test-web-config.md +152 -152
  455. package/templates/skills/utils/subcommands/test-web.md +123 -123
  456. package/templates/skills/validate/SKILL.md +181 -181
  457. package/templates/skills/validate-feature/SKILL.md +101 -101
  458. package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
  459. package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
  460. package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
  461. package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
  462. package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
  463. package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
  464. package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
  465. package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
  466. package/templates/skills/workflow/SKILL.md +127 -127
  467. package/templates/skills/workflow/steps/step-00-init.md +57 -57
  468. package/templates/skills/workflow/steps/step-01-implementation.md +84 -84
  469. package/templates/test-web/api-health.json +38 -38
  470. package/templates/test-web/minimal.json +19 -19
  471. package/templates/test-web/npm-package.json +46 -46
  472. 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)