@specverse/engines 4.1.28 → 4.2.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 (237) hide show
  1. package/assets/examples/manifests/frontend-only.yaml +3 -6
  2. package/assets/examples/manifests/fullstack-app.yaml +5 -7
  3. package/assets/examples/manifests/fullstack-monorepo.yaml +3 -6
  4. package/dist/inference/comprehensive-engine.d.ts.map +1 -1
  5. package/dist/inference/comprehensive-engine.js +3 -19
  6. package/dist/inference/comprehensive-engine.js.map +1 -1
  7. package/dist/inference/core/rule-engine.d.ts +31 -0
  8. package/dist/inference/core/rule-engine.d.ts.map +1 -1
  9. package/dist/inference/core/rule-engine.js +117 -33
  10. package/dist/inference/core/rule-engine.js.map +1 -1
  11. package/dist/inference/core/rule-file-types.d.ts +0 -2
  12. package/dist/inference/core/rule-file-types.d.ts.map +1 -1
  13. package/dist/inference/core/rule-file-types.js +3 -6
  14. package/dist/inference/core/rule-file-types.js.map +1 -1
  15. package/dist/inference/core/rule-loader.d.ts +5 -15
  16. package/dist/inference/core/rule-loader.d.ts.map +1 -1
  17. package/dist/inference/core/rule-loader.js +43 -132
  18. package/dist/inference/core/rule-loader.js.map +1 -1
  19. package/dist/inference/core/types.d.ts +0 -6
  20. package/dist/inference/core/types.d.ts.map +1 -1
  21. package/dist/inference/core/types.js +0 -4
  22. package/dist/inference/core/types.js.map +1 -1
  23. package/dist/inference/logical/generators/component-type-resolver.d.ts +0 -26
  24. package/dist/inference/logical/generators/component-type-resolver.d.ts.map +1 -1
  25. package/dist/inference/logical/generators/component-type-resolver.js +0 -19
  26. package/dist/inference/logical/generators/component-type-resolver.js.map +1 -1
  27. package/dist/inference/logical/generators/specialist-view-expander.d.ts +1 -17
  28. package/dist/inference/logical/generators/specialist-view-expander.d.ts.map +1 -1
  29. package/dist/inference/logical/generators/specialist-view-expander.js +0 -15
  30. package/dist/inference/logical/generators/specialist-view-expander.js.map +1 -1
  31. package/dist/inference/logical/generators/view-generator.d.ts +4 -14
  32. package/dist/inference/logical/generators/view-generator.d.ts.map +1 -1
  33. package/dist/inference/logical/generators/view-generator.js +6 -26
  34. package/dist/inference/logical/generators/view-generator.js.map +1 -1
  35. package/dist/inference/logical/index.d.ts +2 -2
  36. package/dist/inference/logical/index.d.ts.map +1 -1
  37. package/dist/inference/logical/logical-engine.d.ts.map +1 -1
  38. package/dist/inference/logical/logical-engine.js +17 -80
  39. package/dist/inference/logical/logical-engine.js.map +1 -1
  40. package/dist/inference/quint-transpiler.d.ts +5 -3
  41. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  42. package/dist/inference/quint-transpiler.js +11 -6
  43. package/dist/inference/quint-transpiler.js.map +1 -1
  44. package/dist/libs/instance-factories/applications/templates/generic/main-generator.js +3 -3
  45. package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +16 -6
  46. package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +110 -0
  47. package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +121 -0
  48. package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +78 -0
  49. package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +190 -0
  50. package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +45 -0
  51. package/dist/libs/instance-factories/applications/templates/react-starter/html-to-jsx.js +192 -0
  52. package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +46 -0
  53. package/dist/libs/instance-factories/applications/templates/react-starter/orchestrator.js +30 -0
  54. package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +38 -0
  55. package/dist/libs/instance-factories/applications/templates/react-starter/regen-safety.js +89 -0
  56. package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +56 -0
  57. package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +66 -0
  58. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +14 -11
  59. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +11 -3
  60. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +27 -17
  61. package/dist/libs/instance-factories/shared/path-resolver.js +1 -1
  62. package/dist/realize/index.d.ts.map +1 -1
  63. package/dist/realize/index.js +15 -22
  64. package/dist/realize/index.js.map +1 -1
  65. package/dist/registry/utils/manifest-adapter.d.ts +8 -1
  66. package/dist/registry/utils/manifest-adapter.d.ts.map +1 -1
  67. package/dist/registry/utils/manifest-adapter.js +8 -1
  68. package/dist/registry/utils/manifest-adapter.js.map +1 -1
  69. package/libs/instance-factories/applications/react-app-starter.yaml +150 -0
  70. package/libs/instance-factories/applications/templates/generic/main-generator.ts +3 -3
  71. package/libs/instance-factories/applications/templates/react/api-client-generator.ts +16 -6
  72. package/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
  73. package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +153 -0
  74. package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +145 -0
  75. package/libs/instance-factories/applications/templates/react-starter/__tests__/form-body-composer.test.ts +175 -0
  76. package/libs/instance-factories/applications/templates/react-starter/__tests__/helpers-emitter.test.ts +55 -0
  77. package/libs/instance-factories/applications/templates/react-starter/__tests__/html-to-jsx.test.ts +140 -0
  78. package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +146 -0
  79. package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +163 -0
  80. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p2-factory-imports.test.ts +116 -0
  81. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +183 -0
  82. package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts +144 -0
  83. package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +114 -0
  84. package/libs/instance-factories/applications/templates/react-starter/__tests__/view-emitter.test.ts +107 -0
  85. package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +139 -0
  86. package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +141 -0
  87. package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +174 -0
  88. package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +135 -0
  89. package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +306 -0
  90. package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +60 -0
  91. package/libs/instance-factories/applications/templates/react-starter/html-to-jsx.ts +334 -0
  92. package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +120 -0
  93. package/libs/instance-factories/applications/templates/react-starter/orchestrator.ts +80 -0
  94. package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +57 -0
  95. package/libs/instance-factories/applications/templates/react-starter/regen-safety.ts +157 -0
  96. package/libs/instance-factories/applications/templates/react-starter/skeletons/dashboard.tsx.template +47 -0
  97. package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +94 -0
  98. package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +114 -0
  99. package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +72 -0
  100. package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +151 -0
  101. package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +137 -0
  102. package/libs/instance-factories/cli/templates/commander/command-generator.ts +14 -11
  103. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +11 -3
  104. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +27 -17
  105. package/libs/instance-factories/shared/path-resolver.ts +8 -2
  106. package/package.json +3 -3
  107. package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +0 -530
  108. package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +0 -73
  109. package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +0 -99
  110. package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +0 -49
  111. package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +0 -156
  112. package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +0 -935
  113. package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +0 -143
  114. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +0 -646
  115. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +0 -65
  116. package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +0 -143
  117. package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +0 -143
  118. package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +0 -355
  119. package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +0 -91
  120. package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +0 -79
  121. package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js.bak +0 -244
  122. package/dist/libs/instance-factories/views/index.js +0 -48
  123. package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +0 -742
  124. package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +0 -824
  125. package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +0 -719
  126. package/dist/libs/instance-factories/views/templates/react/app-generator.js +0 -45
  127. package/dist/libs/instance-factories/views/templates/react/components-generator.js +0 -820
  128. package/dist/libs/instance-factories/views/templates/react/forms-generator.js +0 -275
  129. package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +0 -46
  130. package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +0 -81
  131. package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +0 -9
  132. package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +0 -23
  133. package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +0 -21
  134. package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +0 -299
  135. package/dist/libs/instance-factories/views/templates/react/router-generator.js +0 -136
  136. package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +0 -107
  137. package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +0 -187
  138. package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +0 -7
  139. package/dist/libs/instance-factories/views/templates/react/types-generator.js +0 -56
  140. package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +0 -27
  141. package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +0 -29
  142. package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +0 -261
  143. package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +0 -34
  144. package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -800
  145. package/dist/libs/instance-factories/views/templates/shared/base-generator.js +0 -305
  146. package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +0 -517
  147. package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
  148. package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +0 -445
  149. package/dist/libs/instance-factories/views/templates/shared/index.js +0 -80
  150. package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +0 -210
  151. package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +0 -492
  152. package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -321
  153. package/dist/realize/index.js.bak +0 -758
  154. package/libs/instance-factories/applications/react-app.yaml +0 -186
  155. package/libs/instance-factories/applications/templates/react/_view-components-source.ts +0 -555
  156. package/libs/instance-factories/applications/templates/react/app-tsx-generator.ts +0 -94
  157. package/libs/instance-factories/applications/templates/react/field-helpers-generator.ts +0 -106
  158. package/libs/instance-factories/applications/templates/react/package-json-generator.ts +0 -57
  159. package/libs/instance-factories/applications/templates/react/pattern-adapter-generator.ts +0 -179
  160. package/libs/instance-factories/applications/templates/react/react-pattern-adapter.tsx +0 -1347
  161. package/libs/instance-factories/applications/templates/react/relationship-field-generator.ts +0 -150
  162. package/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.ts +0 -704
  163. package/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.ts +0 -84
  164. package/libs/instance-factories/applications/templates/react/view-dashboard-generator.ts +0 -150
  165. package/libs/instance-factories/applications/templates/react/view-detail-generator.ts +0 -150
  166. package/libs/instance-factories/applications/templates/react/view-form-generator.ts +0 -362
  167. package/libs/instance-factories/applications/templates/react/view-list-generator.ts +0 -98
  168. package/libs/instance-factories/applications/templates/react/view-router-generator.ts +0 -89
  169. package/libs/instance-factories/views/README.md +0 -62
  170. package/libs/instance-factories/views/index.d.ts +0 -13
  171. package/libs/instance-factories/views/index.d.ts.map +0 -1
  172. package/libs/instance-factories/views/index.js +0 -18
  173. package/libs/instance-factories/views/index.js.map +0 -1
  174. package/libs/instance-factories/views/index.ts +0 -45
  175. package/libs/instance-factories/views/react-components.yaml +0 -129
  176. package/libs/instance-factories/views/templates/ARCHITECTURE.md +0 -198
  177. package/libs/instance-factories/views/templates/react/adapters/antd-adapter.ts +0 -869
  178. package/libs/instance-factories/views/templates/react/adapters/mui-adapter.ts +0 -953
  179. package/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.ts +0 -806
  180. package/libs/instance-factories/views/templates/react/app-generator.ts +0 -55
  181. package/libs/instance-factories/views/templates/react/components-generator.ts +0 -938
  182. package/libs/instance-factories/views/templates/react/forms-generator.ts +0 -325
  183. package/libs/instance-factories/views/templates/react/frontend-package-json-generator.ts +0 -57
  184. package/libs/instance-factories/views/templates/react/hooks-generator.ts +0 -106
  185. package/libs/instance-factories/views/templates/react/index-css-generator.ts +0 -14
  186. package/libs/instance-factories/views/templates/react/index-html-generator.ts +0 -34
  187. package/libs/instance-factories/views/templates/react/main-tsx-generator.ts +0 -29
  188. package/libs/instance-factories/views/templates/react/react-component-generator.d.ts +0 -152
  189. package/libs/instance-factories/views/templates/react/react-component-generator.d.ts.map +0 -1
  190. package/libs/instance-factories/views/templates/react/react-component-generator.js +0 -398
  191. package/libs/instance-factories/views/templates/react/react-component-generator.js.map +0 -1
  192. package/libs/instance-factories/views/templates/react/react-component-generator.ts +0 -533
  193. package/libs/instance-factories/views/templates/react/router-generator.ts +0 -197
  194. package/libs/instance-factories/views/templates/react/router-generic-generator.ts +0 -132
  195. package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +0 -196
  196. package/libs/instance-factories/views/templates/react/spec-json-generator.ts +0 -17
  197. package/libs/instance-factories/views/templates/react/types-generator.ts +0 -76
  198. package/libs/instance-factories/views/templates/react/views-metadata-generator.ts +0 -42
  199. package/libs/instance-factories/views/templates/react/vite-config-generator.ts +0 -38
  200. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.d.ts.map +0 -1
  201. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js.map +0 -1
  202. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.ts +0 -474
  203. package/libs/instance-factories/views/templates/shared/__tests__/composite-patterns.test.ts +0 -242
  204. package/libs/instance-factories/views/templates/shared/adapter-types.d.ts +0 -77
  205. package/libs/instance-factories/views/templates/shared/adapter-types.d.ts.map +0 -1
  206. package/libs/instance-factories/views/templates/shared/adapter-types.js +0 -47
  207. package/libs/instance-factories/views/templates/shared/adapter-types.js.map +0 -1
  208. package/libs/instance-factories/views/templates/shared/adapter-types.ts +0 -142
  209. package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts +0 -63
  210. package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts.map +0 -1
  211. package/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -822
  212. package/libs/instance-factories/views/templates/shared/atomic-components-registry.js.map +0 -1
  213. package/libs/instance-factories/views/templates/shared/atomic-components-registry.ts +0 -908
  214. package/libs/instance-factories/views/templates/shared/base-generator.d.ts +0 -247
  215. package/libs/instance-factories/views/templates/shared/base-generator.d.ts.map +0 -1
  216. package/libs/instance-factories/views/templates/shared/base-generator.js +0 -363
  217. package/libs/instance-factories/views/templates/shared/base-generator.js.map +0 -1
  218. package/libs/instance-factories/views/templates/shared/base-generator.ts +0 -608
  219. package/libs/instance-factories/views/templates/shared/component-metadata.d.ts +0 -254
  220. package/libs/instance-factories/views/templates/shared/component-metadata.d.ts.map +0 -1
  221. package/libs/instance-factories/views/templates/shared/component-metadata.js +0 -602
  222. package/libs/instance-factories/views/templates/shared/component-metadata.js.map +0 -1
  223. package/libs/instance-factories/views/templates/shared/component-metadata.ts +0 -803
  224. package/libs/instance-factories/views/templates/shared/composite-pattern-types.ts +0 -250
  225. package/libs/instance-factories/views/templates/shared/composite-patterns.ts +0 -535
  226. package/libs/instance-factories/views/templates/shared/index.ts +0 -68
  227. package/libs/instance-factories/views/templates/shared/pattern-validator.ts +0 -279
  228. package/libs/instance-factories/views/templates/shared/property-mapper.d.ts +0 -149
  229. package/libs/instance-factories/views/templates/shared/property-mapper.d.ts.map +0 -1
  230. package/libs/instance-factories/views/templates/shared/property-mapper.js +0 -580
  231. package/libs/instance-factories/views/templates/shared/property-mapper.js.map +0 -1
  232. package/libs/instance-factories/views/templates/shared/property-mapper.ts +0 -700
  233. package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts +0 -143
  234. package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts.map +0 -1
  235. package/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -420
  236. package/libs/instance-factories/views/templates/shared/syntax-mapper.js.map +0 -1
  237. package/libs/instance-factories/views/templates/shared/syntax-mapper.ts +0 -539
@@ -1,938 +0,0 @@
1
- /**
2
- * React Components Generator — Pattern-Aware
3
- *
4
- * Generates React components following app-demo's design patterns:
5
- * - Field classification (business, lifecycle, relationship, metadata)
6
- * - Relationship resolution (FK → display name)
7
- * - Lifecycle badges with status colors
8
- * - Smart field ordering and grouping
9
- *
10
- * Components import shared utilities from src/lib/ (generated by shared-utils-generator).
11
- */
12
-
13
- import type { TemplateContext } from '@specverse/types';
14
-
15
- // ============================================================================
16
- // Entry point
17
- // ============================================================================
18
-
19
- export default function generateReactComponent(context: TemplateContext): string {
20
- const { view, model, spec } = context;
21
- if (!view) throw new Error('View is required in template context');
22
-
23
- const componentName = view.name || `${model?.name || 'Unknown'}View`;
24
- let modelName: string;
25
- if (model?.name) modelName = model.name;
26
- else if (Array.isArray(view.model)) modelName = view.model[0];
27
- else if (view.model) modelName = view.model;
28
- else modelName = view.modelReference || 'Unknown';
29
-
30
- const viewType = view.type || 'list';
31
- const lower = modelName.charAt(0).toLowerCase() + modelName.slice(1);
32
- const plural = `${lower}s`;
33
- const api = `/api/${plural}`;
34
- const attrs = getModelAttributes(model);
35
- const belongsTo = getBelongsToRelationships(model);
36
- const hasMany = getHasManyRelationships(model);
37
- const lifecycle = getLifecycle(model);
38
- const classified = classifyAttrs(attrs, lifecycle);
39
-
40
- let body: string;
41
- switch (viewType) {
42
- case 'list':
43
- body = generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
44
- break;
45
- case 'detail':
46
- body = generateDetailView(componentName, modelName, lower, plural, api, classified, belongsTo, hasMany, lifecycle, view);
47
- break;
48
- case 'form':
49
- body = generateFormView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
50
- break;
51
- case 'dashboard':
52
- body = generateDashboardView(componentName, modelName, lower, plural, api, classified, view, model);
53
- break;
54
- case 'board':
55
- case 'workflow':
56
- body = generateBoardView(componentName, modelName, lower, plural, api, lifecycle, view);
57
- break;
58
- case 'timeline':
59
- body = generateTimelineView(componentName, modelName, lower, plural, api, view);
60
- break;
61
- case 'calendar':
62
- body = generateCalendarView(componentName, modelName, lower, plural, api, view, model);
63
- break;
64
- case 'analytics':
65
- body = generateAnalyticsView(componentName, modelName, lower, plural, api, classified, lifecycle, view, model);
66
- break;
67
- default:
68
- body = generateListView(componentName, modelName, lower, plural, api, classified, belongsTo, lifecycle, view);
69
- }
70
- return stripUnusedImports(body);
71
- }
72
-
73
- /**
74
- * Rewrite the top-of-file named imports to drop symbols the body doesn't
75
- * reference. The view generators import every helper unconditionally; this
76
- * pass keeps them clean under tsc's `noUnusedLocals`.
77
- */
78
- function stripUnusedImports(source: string): string {
79
- // Process only the leading contiguous run of named imports.
80
- const lines = source.split('\n');
81
- const out: string[] = [];
82
- let i = 0;
83
- // Find the end of the import block.
84
- while (i < lines.length) {
85
- const line = lines[i];
86
- const match = line.match(/^import\s+\{\s*([^}]+?)\s*\}\s+from\s+(['"][^'"]+['"]);?\s*$/);
87
- if (!match) break;
88
- const names = match[1].split(',').map(s => s.trim()).filter(Boolean);
89
- const from = match[2];
90
- // Join all following lines as the usage body so we can scan for references.
91
- const rest = lines.slice(i + 1).join('\n');
92
- const used = names.filter(n => new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(rest));
93
- if (used.length === 0) {
94
- // drop the import entirely
95
- } else {
96
- out.push(`import { ${used.join(', ')} } from ${from};`);
97
- }
98
- i++;
99
- }
100
- return [...out, ...lines.slice(i)].join('\n');
101
- }
102
-
103
- // ============================================================================
104
- // Model introspection helpers
105
- // ============================================================================
106
-
107
- interface Attr { name: string; type: string; required?: boolean; auto?: string; values?: string[] }
108
- interface Rel { name: string; target: string; type: string; cascade?: boolean }
109
- interface Lifecycle { name: string; states: string[]; statusField: string }
110
- interface ClassifiedAttrs { business: Attr[]; lifecycle: Attr[]; metadata: Attr[]; all: Attr[] }
111
-
112
- function getModelAttributes(model: any): Attr[] {
113
- if (!model?.attributes) return [];
114
- const attrs = Array.isArray(model.attributes) ? model.attributes
115
- : Object.entries(model.attributes).map(([name, def]: [string, any]) => {
116
- if (typeof def === 'string') {
117
- const parts = def.split(' ');
118
- return { name, type: parts[0], required: parts.includes('required'), auto: parts.find((p: string) => p.startsWith('auto='))?.split('=')[1], values: extractValues(def) };
119
- }
120
- return { name, ...def };
121
- });
122
- return attrs;
123
- }
124
-
125
- function extractValues(def: string): string[] | undefined {
126
- const match = def.match(/values=\[([^\]]+)\]/);
127
- if (match) return match[1].split(',').map((s: string) => s.trim());
128
- return undefined;
129
- }
130
-
131
- function getBelongsToRelationships(model: any): Rel[] {
132
- if (!model?.relationships) return [];
133
- const rels = Array.isArray(model.relationships) ? model.relationships
134
- : Object.entries(model.relationships).map(([name, def]: [string, any]) => {
135
- if (typeof def === 'string') {
136
- const parts = def.split(' ');
137
- return { name, type: parts[0], target: parts[1], cascade: parts.includes('cascade') };
138
- }
139
- return { name, ...def };
140
- });
141
- return rels.filter((r: any) => r.type === 'belongsTo');
142
- }
143
-
144
- function getHasManyRelationships(model: any): Rel[] {
145
- if (!model?.relationships) return [];
146
- const rels = Array.isArray(model.relationships) ? model.relationships
147
- : Object.entries(model.relationships).map(([name, def]: [string, any]) => {
148
- if (typeof def === 'string') {
149
- const parts = def.split(' ');
150
- return { name, type: parts[0], target: parts[1] };
151
- }
152
- return { name, ...def };
153
- });
154
- return rels.filter((r: any) => r.type === 'hasMany');
155
- }
156
-
157
- function getLifecycle(model: any): Lifecycle | null {
158
- if (!model?.lifecycles) return null;
159
- const entries = Array.isArray(model.lifecycles) ? model.lifecycles
160
- : Object.entries(model.lifecycles).map(([name, lc]: [string, any]) => ({ name, ...lc }));
161
- if (entries.length === 0) return null;
162
- const lc = entries[0];
163
- const states = lc.states || lc.flow?.split(/\s*->\s*/) || [];
164
- return { name: lc.name || 'status', states, statusField: lc.name || 'status' };
165
- }
166
-
167
- const METADATA_FIELDS = new Set(['id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'deletedAt', 'version']);
168
-
169
- function classifyAttrs(attrs: Attr[], lifecycle: Lifecycle | null): ClassifiedAttrs {
170
- const business: Attr[] = [];
171
- const lifecycleAttrs: Attr[] = [];
172
- const metadata: Attr[] = [];
173
- for (const a of attrs) {
174
- if (a.auto || METADATA_FIELDS.has(a.name)) metadata.push(a);
175
- else if (lifecycle && a.name === lifecycle.statusField) lifecycleAttrs.push(a);
176
- else if (['status', 'state', 'phase'].includes(a.name)) lifecycleAttrs.push(a);
177
- else business.push(a);
178
- }
179
- return { business, lifecycle: lifecycleAttrs, metadata, all: attrs };
180
- }
181
-
182
- function pluralize(s: string): string {
183
- if (s.endsWith('s')) return s;
184
- if (s.endsWith('y')) return s.slice(0, -1) + 'ies';
185
- return s + 's';
186
- }
187
-
188
- // ============================================================================
189
- // LIST VIEW
190
- // ============================================================================
191
-
192
- function generateListView(name: string, model: string, lower: string, plural: string, api: string, classified: ClassifiedAttrs, belongsTo: Rel[], lifecycle: Lifecycle | null, view: any): string {
193
- const displayCols = classified.business.slice(0, 5);
194
- const statusField = lifecycle?.statusField || classified.lifecycle[0]?.name;
195
- const apiPath = api.replace(/^\/api/, ''); // strip /api prefix for apiRequest
196
-
197
- // Build relationship lookup fetches
198
- const relFetches = belongsTo.map(r => {
199
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
200
- return ` const [${tLower}Map, set${r.target}Map] = useState<Record<string, any>>({});`;
201
- }).join('\n');
202
-
203
- const relEffects = belongsTo.map(r => {
204
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
205
- return ` apiRequest('GET', '/${tLower}s').then(data => {
206
- if (Array.isArray(data)) {
207
- const m: Record<string, any> = {};
208
- data.forEach(e => { m[e.id] = e; });
209
- set${r.target}Map(m);
210
- }
211
- }).catch(() => {});`;
212
- }).join('\n');
213
-
214
- const relColumns = belongsTo.map(r => {
215
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
216
- const fk = `${r.name}_id`;
217
- return { header: r.target, cell: `{${tLower}Map[item.${fk}] ? getEntityDisplayName(${tLower}Map[item.${fk}]) : (item.${fk} ? item.${fk}.slice(0,8)+'...' : '—')}` };
218
- });
219
-
220
- return `import { useState, useEffect } from 'react';
221
- import { Link } from 'react-router-dom';
222
- import { apiRequest } from '../lib/apiClient';
223
- import { getEntityDisplayName } from '../lib/field-helpers';
224
- import { formatValue, formatDate, StatusBadge } from '../lib/view-helpers';
225
-
226
- function ${name}() {
227
- const [items, setItems] = useState<any[]>([]);
228
- const [loading, setLoading] = useState(true);
229
- ${relFetches}
230
-
231
- useEffect(() => {
232
- apiRequest('GET', '${apiPath}').then(data => {
233
- setItems(Array.isArray(data) ? data : []);
234
- setLoading(false);
235
- }).catch(() => setLoading(false));
236
- ${relEffects}
237
- }, []);
238
-
239
- if (loading) return <div className="flex items-center justify-center h-64 text-gray-400">Loading...</div>;
240
-
241
- return (
242
- <div className="p-6">
243
- <div className="flex justify-between items-center mb-6">
244
- <div>
245
- <h1 className="text-2xl font-bold text-gray-900">${pluralize(model)}</h1>
246
- <p className="text-sm text-gray-500 mt-1">{items.length} ${lower}{items.length !== 1 ? 's' : ''}</p>
247
- </div>
248
- <Link to="/${lower}form" className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors">
249
- + New ${model}
250
- </Link>
251
- </div>
252
-
253
- {items.length === 0 ? (
254
- <div className="text-center py-16 bg-white rounded-lg border border-dashed border-gray-300">
255
- <p className="text-gray-500">No ${plural} yet</p>
256
- <Link to="/${lower}form" className="mt-3 inline-block text-blue-600 hover:text-blue-700 text-sm font-medium">Create your first ${lower} →</Link>
257
- </div>
258
- ) : (
259
- <div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
260
- <table className="min-w-full divide-y divide-gray-200">
261
- <thead className="bg-gray-50 sticky top-0">
262
- <tr>
263
- ${displayCols.map(c => ` <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">${c.name}</th>`).join('\n')}
264
- ${relColumns.map(r => ` <th className="px-4 py-3 text-left text-xs font-medium text-blue-600 uppercase tracking-wider">${r.header}</th>`).join('\n')}
265
- ${statusField ? ` <th className="px-4 py-3 text-left text-xs font-medium text-purple-600 uppercase tracking-wider">Status</th>` : ''}
266
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider italic">Created</th>
267
- <th className="px-4 py-3"></th>
268
- </tr>
269
- </thead>
270
- <tbody className="bg-white divide-y divide-gray-200">
271
- {items.map((item) => (
272
- <tr key={item.id} className="hover:bg-gray-50 cursor-pointer transition-colors" onClick={() => window.location.href = \`/${lower}detail?id=\${item.id}\`}>
273
- ${displayCols.map(c => ` <td className="px-4 py-3 text-sm text-gray-900">{formatValue(item.${c.name}, '${c.type}')}</td>`).join('\n')}
274
- ${relColumns.map(r => ` <td className="px-4 py-3 text-sm text-blue-700">${r.cell}</td>`).join('\n')}
275
- ${statusField ? ` <td className="px-4 py-3"><StatusBadge status={item.${statusField}} variant="lifecycle" /></td>` : ''}
276
- <td className="px-4 py-3 text-xs text-gray-400 font-mono">{formatDate(item.createdAt)}</td>
277
- <td className="px-4 py-3 text-right">
278
- <Link to={\`/${lower}form?id=\${item.id}\`} className="text-blue-600 hover:text-blue-800 text-sm font-medium" onClick={e => e.stopPropagation()}>Edit</Link>
279
- </td>
280
- </tr>
281
- ))}
282
- </tbody>
283
- </table>
284
- </div>
285
- )}
286
- </div>
287
- );
288
- }
289
-
290
- export default ${name};
291
- `;
292
- }
293
-
294
- // ============================================================================
295
- // DETAIL VIEW
296
- // ============================================================================
297
-
298
- function generateDetailView(name: string, model: string, lower: string, plural: string, api: string, classified: ClassifiedAttrs, belongsTo: Rel[], hasMany: Rel[], lifecycle: Lifecycle | null, view: any): string {
299
- const statusField = lifecycle?.statusField || classified.lifecycle[0]?.name;
300
- const apiPath = api.replace(/^\/api/, '');
301
-
302
- const relFetches = belongsTo.map(r => {
303
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
304
- return ` const [${tLower}Ref, set${r.target}Ref] = useState<any>(null);`;
305
- }).join('\n');
306
-
307
- const relEffects = belongsTo.map(r => {
308
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
309
- const fk = `${r.name}_id`;
310
- return ` if (data.${fk}) apiRequest('GET', \`/${tLower}s/\${data.${fk}}\`).then(d => set${r.target}Ref(d)).catch(() => {});`;
311
- }).join('\n');
312
-
313
- const hasManyFetches = hasMany.map(r => {
314
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
315
- return ` const [${r.name}Items, set${capitalize(r.name)}Items] = useState<any[]>([]);`;
316
- }).join('\n');
317
-
318
- const hasManyEffects = hasMany.map(r => {
319
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
320
- return ` apiRequest('GET', \`/${tLower}s?${lower}Id=\${data.id}\`).then(d => set${capitalize(r.name)}Items(Array.isArray(d) ? d : [])).catch(() => {});`;
321
- }).join('\n');
322
-
323
- return `import { useState, useEffect } from 'react';
324
- import { Link, useSearchParams, useNavigate } from 'react-router-dom';
325
- import { apiRequest } from '../lib/apiClient';
326
- import { getEntityDisplayName } from '../lib/field-helpers';
327
- import { formatValue, formatDate, StatusBadge } from '../lib/view-helpers';
328
-
329
- function ${name}() {
330
- const [searchParams] = useSearchParams();
331
- const navigate = useNavigate();
332
- const id = searchParams.get('id');
333
- const [item, setItem] = useState<any>(null);
334
- const [loading, setLoading] = useState(true);
335
- ${relFetches}
336
- ${hasManyFetches}
337
-
338
- useEffect(() => {
339
- if (!id) { setLoading(false); return; }
340
- apiRequest('GET', \`${apiPath}/\${id}\`).then(data => {
341
- setItem(data);
342
- setLoading(false);
343
- if (data) {
344
- ${relEffects}
345
- ${hasManyEffects}
346
- }
347
- }).catch(() => setLoading(false));
348
- }, [id]);
349
-
350
- if (loading) return <div className="flex items-center justify-center h-64 text-gray-400">Loading...</div>;
351
- if (!item) return <div className="p-6 text-gray-500">Not found</div>;
352
-
353
- const handleDelete = async () => {
354
- if (!confirm('Delete this ${lower}?')) return;
355
- await apiRequest('DELETE', \`${apiPath}/\${id}\`);
356
- navigate('/${lower}list');
357
- };
358
-
359
- return (
360
- <div className="p-6 max-w-3xl">
361
- {/* Header */}
362
- <div className="flex items-start justify-between mb-6">
363
- <div>
364
- <h1 className="text-2xl font-bold text-gray-900">{getEntityDisplayName(item)}</h1>
365
- ${statusField ? ` <div className="mt-2"><StatusBadge status={item.${statusField}} /></div>` : ''}
366
- </div>
367
- <div className="flex gap-2">
368
- <Link to={\`/${lower}form?id=\${id}\`} className="px-3 py-1.5 text-sm font-medium text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50">Edit</Link>
369
- <button onClick={handleDelete} className="px-3 py-1.5 text-sm font-medium text-red-600 border border-red-200 rounded-lg hover:bg-red-50">Delete</button>
370
- </div>
371
- </div>
372
-
373
- {/* Business Fields */}
374
- <div className="bg-white rounded-lg border border-gray-200 shadow-sm mb-6">
375
- <div className="divide-y divide-gray-100">
376
- ${classified.business.map(a => ` <div className="px-5 py-3 flex justify-between">
377
- <span className="text-sm text-gray-500">${a.name.charAt(0).toUpperCase() + a.name.slice(1)}</span>
378
- <span className="text-sm text-gray-900">{formatValue(item.${a.name}, '${a.type}')}</span>
379
- </div>`).join('\n')}
380
- </div>
381
- </div>
382
-
383
- ${belongsTo.length > 0 ? ` {/* Relationships */}
384
- <div className="bg-white rounded-lg border border-gray-200 shadow-sm mb-6">
385
- <div className="px-5 py-3 border-b border-gray-100">
386
- <h3 className="text-sm font-medium text-gray-700">Related</h3>
387
- </div>
388
- <div className="divide-y divide-gray-100">
389
- ${belongsTo.map(r => {
390
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
391
- return ` <div className="px-5 py-3 flex justify-between items-center">
392
- <span className="text-sm text-gray-500">${r.target}</span>
393
- {${tLower}Ref ? (
394
- <Link to={\`/${tLower}detail?id=\${${tLower}Ref.id}\`} className="text-sm text-blue-600 hover:text-blue-800">{getEntityDisplayName(${tLower}Ref)}</Link>
395
- ) : <span className="text-sm text-gray-400">—</span>}
396
- </div>`;
397
- }).join('\n')}
398
- </div>
399
- </div>` : ''}
400
-
401
- ${hasMany.map(r => {
402
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
403
- return ` {/* ${r.target} */}
404
- <div className="bg-white rounded-lg border border-gray-200 shadow-sm mb-6">
405
- <div className="px-5 py-3 border-b border-gray-100 flex justify-between items-center">
406
- <h3 className="text-sm font-medium text-gray-700">${pluralize(r.target)} ({${r.name}Items.length})</h3>
407
- <Link to={\`/${tLower}form?${lower}Id=\${id}\`} className="text-xs text-blue-600 hover:text-blue-800">+ Add</Link>
408
- </div>
409
- <ul className="divide-y divide-gray-100">
410
- {${r.name}Items.slice(0, 10).map((child: any, i: number) => (
411
- <li key={i} className="px-5 py-2.5 flex justify-between items-center hover:bg-gray-50">
412
- <Link to={\`/${tLower}detail?id=\${child.id}\`} className="text-sm text-gray-900 hover:text-blue-600">{getEntityDisplayName(child)}</Link>
413
- {child.status && <StatusBadge status={child.status} />}
414
- </li>
415
- ))}
416
- {${r.name}Items.length === 0 && <li className="px-5 py-3 text-sm text-gray-400">None yet</li>}
417
- </ul>
418
- </div>`;
419
- }).join('\n')}
420
-
421
- {/* Metadata */}
422
- <div className="text-xs text-gray-400 space-y-1">
423
- <div>ID: {item.id}</div>
424
- {item.createdAt && <div>Created: {formatDate(item.createdAt)}</div>}
425
- {item.updatedAt && <div>Updated: {formatDate(item.updatedAt)}</div>}
426
- </div>
427
- </div>
428
- );
429
- }
430
-
431
- export default ${name};
432
- `;
433
- }
434
-
435
- // ============================================================================
436
- // FORM VIEW
437
- // ============================================================================
438
-
439
- function generateFormView(name: string, model: string, lower: string, plural: string, api: string, classified: ClassifiedAttrs, belongsTo: Rel[], lifecycle: Lifecycle | null, view: any): string {
440
- // Only show editable fields (business + lifecycle, not metadata/auto)
441
- const editableFields = [...classified.business, ...classified.lifecycle];
442
- const statusField = lifecycle?.statusField;
443
- const apiPath = api.replace(/^\/api/, '');
444
-
445
- const relStates = belongsTo.map(r => {
446
- return ` const [${r.target.charAt(0).toLowerCase() + r.target.slice(1)}Options, set${r.target}Options] = useState<any[]>([]);`;
447
- }).join('\n');
448
-
449
- const relEffects = belongsTo.map(r => {
450
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
451
- return ` apiRequest('GET', '/${tLower}s').then(d => set${r.target}Options(Array.isArray(d) ? d : [])).catch(() => {});`;
452
- }).join('\n');
453
-
454
- function inputForField(attr: Attr): string {
455
- const { name: n, type, required, values } = attr;
456
- const req = required ? ' required' : '';
457
-
458
- // Enum dropdown
459
- if (values && values.length > 0) {
460
- return ` <select name="${n}" value={form.${n} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req}>
461
- <option value="">Select...</option>
462
- ${values.map(v => ` <option value="${v}">${v}</option>`).join('\n')}
463
- </select>`;
464
- }
465
-
466
- // Lifecycle status dropdown
467
- if (lifecycle && n === statusField) {
468
- return ` <select name="${n}" value={form.${n} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 border-l-4 border-l-purple-600 rounded text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"${req}>
469
- <option value="">Select status...</option>
470
- ${lifecycle.states.map(s => ` <option value="${s}">${s.replace(/[_-]/g, ' ')}</option>`).join('\n')}
471
- </select>`;
472
- }
473
-
474
- const t = type.toLowerCase();
475
- if (t === 'boolean') return ` <input type="checkbox" name="${n}" checked={!!form.${n}} onChange={e => setForm((f: any) => ({...f, ${n}: e.target.checked}))} className="h-4 w-4 text-blue-600 rounded" />`;
476
- if (t.includes('date') || t.includes('timestamp')) return ` <input type="datetime-local" name="${n}" value={form.${n} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
477
- if (t === 'integer' || t === 'number' || t === 'money' || t === 'decimal' || t === 'float') return ` <input type="number" name="${n}" value={form.${n} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
478
- if (n.toLowerCase().includes('description') || n.toLowerCase().includes('content') || n.toLowerCase().includes('body')) return ` <textarea name="${n}" value={form.${n} || ''} onChange={handleChange} rows={4} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
479
- if (t === 'email') return ` <input type="email" name="${n}" value={form.${n} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
480
- return ` <input type="text" name="${n}" value={form.${n} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"${req} />`;
481
- }
482
-
483
- return `import { useState, useEffect } from 'react';
484
- import { useSearchParams, useNavigate } from 'react-router-dom';
485
- import { apiRequest } from '../lib/apiClient';
486
- import { getEntityDisplayName } from '../lib/field-helpers';
487
-
488
- function ${name}() {
489
- const [searchParams] = useSearchParams();
490
- const navigate = useNavigate();
491
- const editId = searchParams.get('id');
492
- const [form, setForm] = useState<any>({});
493
- const [error, setError] = useState('');
494
- const [saving, setSaving] = useState(false);
495
- ${relStates}
496
-
497
- useEffect(() => {
498
- if (editId) {
499
- apiRequest('GET', \`${apiPath}/\${editId}\`).then(data => setForm(data || {})).catch(() => {});
500
- }
501
- ${relEffects}
502
- }, [editId]);
503
-
504
- const handleChange = (e: any) => {
505
- const { name, value, type } = e.target;
506
- setForm((f: any) => ({ ...f, [name]: type === 'number' ? Number(value) : value }));
507
- };
508
-
509
- const handleSubmit = async (e: any) => {
510
- e.preventDefault();
511
- setSaving(true);
512
- setError('');
513
- try {
514
- if (editId) {
515
- await apiRequest('PUT', \`${apiPath}/\${editId}\`, form);
516
- } else {
517
- await apiRequest('POST', '${apiPath}', form);
518
- }
519
- // apiRequest throws on non-2xx so reaching here means success.
520
- navigate('/${lower}list');
521
- } catch (err: any) {
522
- setError(err.message);
523
- } finally {
524
- setSaving(false);
525
- }
526
- };
527
-
528
- return (
529
- <div className="p-6 max-w-2xl">
530
- <h1 className="text-2xl font-bold text-gray-900 mb-6">{editId ? 'Edit' : 'New'} ${model}</h1>
531
-
532
- {error && (
533
- <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{error}</div>
534
- )}
535
-
536
- <form onSubmit={handleSubmit} className="bg-white rounded-lg border border-gray-200 shadow-sm">
537
- <div className="p-6 space-y-5">
538
- ${editableFields.map(a => ` {/* ${a.name} */}
539
- <div>
540
- <label className="block text-sm font-medium text-gray-700 mb-1">
541
- ${a.name.charAt(0).toUpperCase() + a.name.slice(1)}${a.required ? ' *' : ''}
542
- </label>
543
- ${inputForField(a)}
544
- </div>`).join('\n\n')}
545
-
546
- ${belongsTo.map(r => {
547
- const tLower = r.target.charAt(0).toLowerCase() + r.target.slice(1);
548
- const fk = `${r.name}_id`;
549
- return ` {/* ${r.target} (relationship) */}
550
- <div>
551
- <label className="block text-sm font-medium text-gray-700 mb-1">${r.target}</label>
552
- <select name="${fk}" value={form.${fk} || ''} onChange={handleChange} className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
553
- <option value="">Select ${r.target}...</option>
554
- {${tLower}Options.map((opt: any) => (
555
- <option key={opt.id} value={opt.id}>{getEntityDisplayName(opt)}</option>
556
- ))}
557
- </select>
558
- </div>`;
559
- }).join('\n\n')}
560
- </div>
561
-
562
- <div className="px-6 py-4 bg-gray-50 border-t border-gray-200 rounded-b-lg flex justify-end gap-3">
563
- <button type="button" onClick={() => navigate(-1)} className="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-100">
564
- Cancel
565
- </button>
566
- <button type="submit" disabled={saving} className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50">
567
- {saving ? 'Saving...' : (editId ? 'Update' : 'Create')}
568
- </button>
569
- </div>
570
- </form>
571
- </div>
572
- );
573
- }
574
-
575
- export default ${name};
576
- `;
577
- }
578
-
579
- // ============================================================================
580
- // DASHBOARD VIEW
581
- // ============================================================================
582
-
583
- function generateDashboardView(name: string, model: string, lower: string, plural: string, api: string, classified: ClassifiedAttrs, view: any, modelDef: any): string {
584
- const numericAttrs = classified.business.filter(a => ['integer', 'number', 'money', 'decimal', 'float'].includes(a.type.toLowerCase()));
585
- const metricNames = numericAttrs.map(a => a.name).slice(0, 4);
586
- const apiPath = api.replace(/^\/api/, '');
587
-
588
- return `import { useState, useEffect } from 'react';
589
- import { apiRequest } from '../lib/apiClient';
590
- import { getEntityDisplayName } from '../lib/field-helpers';
591
- import { formatDate, StatusBadge } from '../lib/view-helpers';
592
-
593
- function ${name}() {
594
- const [items, setItems] = useState<any[]>([]);
595
- const [loading, setLoading] = useState(true);
596
-
597
- useEffect(() => {
598
- apiRequest('GET', '${apiPath}').then(data => {
599
- setItems(Array.isArray(data) ? data : []);
600
- setLoading(false);
601
- }).catch(() => setLoading(false));
602
- }, []);
603
-
604
- if (loading) return <div className="flex items-center justify-center h-64 text-gray-400">Loading...</div>;
605
-
606
- const total = items.length;
607
- ${metricNames.map(m => ` const ${m}Total = items.reduce((sum, item) => sum + (Number(item.${m}) || 0), 0);`).join('\n')}
608
-
609
- return (
610
- <div className="p-6 space-y-6">
611
- <h1 className="text-2xl font-bold text-gray-900">${model} Dashboard</h1>
612
-
613
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
614
- <div className="bg-white rounded-lg border border-gray-200 shadow-sm p-5">
615
- <p className="text-sm text-gray-500">Total</p>
616
- <p className="text-3xl font-bold text-blue-600">{total}</p>
617
- </div>
618
- ${metricNames.map(m => ` <div className="bg-white rounded-lg border border-gray-200 shadow-sm p-5">
619
- <p className="text-sm text-gray-500">${m.charAt(0).toUpperCase() + m.slice(1)}</p>
620
- <p className="text-3xl font-bold text-blue-600">{${m}Total}</p>
621
- </div>`).join('\n')}
622
- </div>
623
-
624
- <div className="bg-white rounded-lg border border-gray-200 shadow-sm">
625
- <div className="px-5 py-4 border-b border-gray-200">
626
- <h2 className="text-lg font-medium text-gray-900">Recent</h2>
627
- </div>
628
- <ul className="divide-y divide-gray-100">
629
- {items.slice(0, 8).map((item, i) => (
630
- <li key={i} className="px-5 py-3 flex justify-between items-center">
631
- <span className="text-sm text-gray-900">{getEntityDisplayName(item)}</span>
632
- <div className="flex items-center gap-3">
633
- {item.status && <StatusBadge status={item.status} />}
634
- <span className="text-xs text-gray-400">{formatDate(item.createdAt)}</span>
635
- </div>
636
- </li>
637
- ))}
638
- {items.length === 0 && <li className="px-5 py-3 text-sm text-gray-400">No ${plural} yet</li>}
639
- </ul>
640
- </div>
641
- </div>
642
- );
643
- }
644
-
645
- export default ${name};
646
- `;
647
- }
648
-
649
- // ============================================================================
650
- // BOARD VIEW (lifecycle kanban)
651
- // ============================================================================
652
-
653
- function generateBoardView(name: string, model: string, lower: string, plural: string, api: string, lifecycle: Lifecycle | null, view: any): string {
654
- const states = lifecycle?.states || ['todo', 'in_progress', 'done'];
655
- const statusField = lifecycle?.statusField || 'status';
656
- const apiPath = api.replace(/^\/api/, '');
657
-
658
- return `import { useState, useEffect } from 'react';
659
- import { Link } from 'react-router-dom';
660
- import { apiRequest } from '../lib/apiClient';
661
- import { getEntityDisplayName } from '../lib/field-helpers';
662
- import { statusColor } from '../lib/view-helpers';
663
-
664
- function ${name}() {
665
- const [items, setItems] = useState<any[]>([]);
666
- const [loading, setLoading] = useState(true);
667
-
668
- useEffect(() => {
669
- apiRequest('GET', '${apiPath}').then(data => {
670
- setItems(Array.isArray(data) ? data : []);
671
- setLoading(false);
672
- }).catch(() => setLoading(false));
673
- }, []);
674
-
675
- if (loading) return <div className="flex items-center justify-center h-64 text-gray-400">Loading...</div>;
676
-
677
- const columns = ${JSON.stringify(states)};
678
- const grouped: Record<string, any[]> = {};
679
- columns.forEach(col => { grouped[col] = []; });
680
- items.forEach(item => {
681
- const col = item.${statusField} || columns[0];
682
- if (grouped[col]) grouped[col].push(item);
683
- else grouped[columns[columns.length - 1]]?.push(item);
684
- });
685
-
686
- return (
687
- <div className="p-6">
688
- <h1 className="text-2xl font-bold text-gray-900 mb-6">${model} Board</h1>
689
- <div className="flex gap-4 overflow-x-auto pb-4">
690
- {columns.map(col => {
691
- const colors = statusColor(col);
692
- return (
693
- <div key={col} className="flex-shrink-0 w-72 bg-gray-50 rounded-lg border border-gray-200">
694
- <div className="p-3 border-b border-gray-200 flex items-center justify-between">
695
- <div className="flex items-center gap-2">
696
- <span className={\`w-2 h-2 rounded-full \${colors.dot}\`}></span>
697
- <h3 className="text-sm font-semibold text-gray-700">{col.replace(/[_-]/g, ' ')}</h3>
698
- </div>
699
- <span className="text-xs text-gray-400 font-medium">{(grouped[col] || []).length}</span>
700
- </div>
701
- <div className="p-2 space-y-2 min-h-[200px]">
702
- {(grouped[col] || []).map((item, i) => (
703
- <Link key={i} to={\`/${lower}detail?id=\${item.id}\`} className="block bg-white rounded-md border border-gray-200 p-3 hover:shadow-md transition-shadow">
704
- <p className="text-sm font-medium text-gray-900">{getEntityDisplayName(item)}</p>
705
- {item.description && <p className="text-xs text-gray-500 mt-1 line-clamp-2">{item.description}</p>}
706
- </Link>
707
- ))}
708
- </div>
709
- </div>
710
- );
711
- })}
712
- </div>
713
- </div>
714
- );
715
- }
716
-
717
- export default ${name};
718
- `;
719
- }
720
-
721
- // ============================================================================
722
- // TIMELINE VIEW
723
- // ============================================================================
724
-
725
- function generateTimelineView(name: string, model: string, lower: string, plural: string, api: string, view: any): string {
726
- const apiPath = api.replace(/^\/api/, '');
727
- return `import { useState, useEffect } from 'react';
728
- import { Link } from 'react-router-dom';
729
- import { apiRequest } from '../lib/apiClient';
730
- import { getEntityDisplayName } from '../lib/field-helpers';
731
- import { formatDate, StatusBadge } from '../lib/view-helpers';
732
-
733
- function ${name}() {
734
- const [items, setItems] = useState<any[]>([]);
735
- const [loading, setLoading] = useState(true);
736
-
737
- useEffect(() => {
738
- apiRequest('GET', '${apiPath}').then(data => {
739
- const sorted = (Array.isArray(data) ? data : []).sort((a, b) =>
740
- new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime()
741
- );
742
- setItems(sorted);
743
- setLoading(false);
744
- }).catch(() => setLoading(false));
745
- }, []);
746
-
747
- if (loading) return <div className="flex items-center justify-center h-64 text-gray-400">Loading...</div>;
748
-
749
- return (
750
- <div className="p-6 max-w-3xl">
751
- <h1 className="text-2xl font-bold text-gray-900 mb-6">${model} Timeline</h1>
752
- <div className="relative">
753
- <div className="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-200"></div>
754
- <div className="space-y-4">
755
- {items.map((item, i) => (
756
- <div key={i} className="relative flex items-start ml-4 pl-6">
757
- <div className="absolute -left-1.5 mt-2 w-3 h-3 rounded-full bg-blue-500 border-2 border-white shadow"></div>
758
- <Link to={\`/${lower}detail?id=\${item.id}\`} className="bg-white rounded-lg border border-gray-200 shadow-sm p-4 flex-1 hover:shadow-md transition-shadow">
759
- <div className="flex justify-between items-start">
760
- <h3 className="text-sm font-medium text-gray-900">{getEntityDisplayName(item)}</h3>
761
- <time className="text-xs text-gray-400 ml-4">{formatDate(item.createdAt)}</time>
762
- </div>
763
- {item.description && <p className="text-sm text-gray-500 mt-1 line-clamp-2">{item.description}</p>}
764
- {item.status && <div className="mt-2"><StatusBadge status={item.status} /></div>}
765
- </Link>
766
- </div>
767
- ))}
768
- {items.length === 0 && <p className="ml-10 text-sm text-gray-400">No ${plural} yet</p>}
769
- </div>
770
- </div>
771
- </div>
772
- );
773
- }
774
-
775
- export default ${name};
776
- `;
777
- }
778
-
779
- // ============================================================================
780
- // CALENDAR VIEW
781
- // ============================================================================
782
-
783
- function generateCalendarView(name: string, model: string, lower: string, plural: string, api: string, view: any, modelDef: any): string {
784
- const attrs = getModelAttributes(modelDef);
785
- const dateField = attrs.find(a =>
786
- ['startdate', 'duedate', 'scheduledat', 'eventdate', 'date'].includes(a.name.toLowerCase())
787
- )?.name || 'createdAt';
788
- const apiPath = api.replace(/^\/api/, '');
789
-
790
- return `import { useState, useEffect } from 'react';
791
- import { apiRequest } from '../lib/apiClient';
792
- import { getEntityDisplayName } from '../lib/field-helpers';
793
-
794
- function ${name}() {
795
- const [items, setItems] = useState<any[]>([]);
796
- const [loading, setLoading] = useState(true);
797
- const [currentMonth, setCurrentMonth] = useState(new Date());
798
-
799
- useEffect(() => {
800
- apiRequest('GET', '${apiPath}').then(data => {
801
- setItems(Array.isArray(data) ? data : []);
802
- setLoading(false);
803
- }).catch(() => setLoading(false));
804
- }, []);
805
-
806
- if (loading) return <div className="flex items-center justify-center h-64 text-gray-400">Loading...</div>;
807
-
808
- const year = currentMonth.getFullYear();
809
- const month = currentMonth.getMonth();
810
- const firstDay = new Date(year, month, 1).getDay();
811
- const daysInMonth = new Date(year, month + 1, 0).getDate();
812
- const days = Array.from({ length: daysInMonth }, (_, i) => i + 1);
813
- const blanks = Array.from({ length: firstDay }, (_, i) => i);
814
-
815
- const getItemsForDay = (day: number) => items.filter(item => {
816
- const d = new Date(item.${dateField} || item.createdAt);
817
- return d.getFullYear() === year && d.getMonth() === month && d.getDate() === day;
818
- });
819
-
820
- return (
821
- <div className="p-6">
822
- <div className="flex items-center justify-between mb-6">
823
- <h1 className="text-2xl font-bold text-gray-900">${model} Calendar</h1>
824
- <div className="flex items-center gap-3">
825
- <button onClick={() => setCurrentMonth(new Date(year, month - 1, 1))} className="p-2 hover:bg-gray-100 rounded-lg text-gray-600">&larr;</button>
826
- <span className="text-lg font-medium text-gray-900 w-48 text-center">{currentMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}</span>
827
- <button onClick={() => setCurrentMonth(new Date(year, month + 1, 1))} className="p-2 hover:bg-gray-100 rounded-lg text-gray-600">&rarr;</button>
828
- </div>
829
- </div>
830
- <div className="grid grid-cols-7 gap-px bg-gray-200 rounded-lg overflow-hidden border border-gray-200">
831
- {['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].map(d => (
832
- <div key={d} className="bg-gray-50 p-2 text-xs font-medium text-gray-500 text-center">{d}</div>
833
- ))}
834
- {blanks.map(i => <div key={\`b\${i}\`} className="bg-white p-2 min-h-[80px]"></div>)}
835
- {days.map(day => {
836
- const dayItems = getItemsForDay(day);
837
- const isToday = day === new Date().getDate() && month === new Date().getMonth() && year === new Date().getFullYear();
838
- return (
839
- <div key={day} className="bg-white p-2 min-h-[80px]">
840
- <span className={\`text-sm \${isToday ? 'bg-blue-600 text-white w-6 h-6 rounded-full inline-flex items-center justify-center' : dayItems.length > 0 ? 'font-bold text-blue-600' : 'text-gray-700'}\`}>{day}</span>
841
- {dayItems.slice(0, 2).map((item, i) => (
842
- <div key={i} className="mt-1 text-xs bg-blue-50 text-blue-700 rounded px-1.5 py-0.5 truncate">{getEntityDisplayName(item)}</div>
843
- ))}
844
- {dayItems.length > 2 && <div className="mt-1 text-xs text-gray-400">+{dayItems.length - 2} more</div>}
845
- </div>
846
- );
847
- })}
848
- </div>
849
- </div>
850
- );
851
- }
852
-
853
- export default ${name};
854
- `;
855
- }
856
-
857
- // ============================================================================
858
- // ANALYTICS VIEW
859
- // ============================================================================
860
-
861
- function generateAnalyticsView(name: string, model: string, lower: string, plural: string, api: string, classified: ClassifiedAttrs, lifecycle: Lifecycle | null, view: any, modelDef: any): string {
862
- const numericAttrs = classified.business.filter(a => ['integer', 'number', 'money', 'decimal', 'float'].includes(a.type.toLowerCase()));
863
- const statusField = lifecycle?.statusField || 'status';
864
- const apiPath = api.replace(/^\/api/, '');
865
-
866
- return `import { useState, useEffect } from 'react';
867
- import { apiRequest } from '../lib/apiClient';
868
-
869
- function ${name}() {
870
- const [items, setItems] = useState<any[]>([]);
871
- const [loading, setLoading] = useState(true);
872
-
873
- useEffect(() => {
874
- apiRequest('GET', '${apiPath}').then(data => {
875
- setItems(Array.isArray(data) ? data : []);
876
- setLoading(false);
877
- }).catch(() => setLoading(false));
878
- }, []);
879
-
880
- if (loading) return <div className="flex items-center justify-center h-64 text-gray-400">Loading...</div>;
881
-
882
- const statusCounts: Record<string, number> = {};
883
- items.forEach(item => {
884
- const s = item.${statusField} || 'unknown';
885
- statusCounts[s] = (statusCounts[s] || 0) + 1;
886
- });
887
- const maxCount = Math.max(...Object.values(statusCounts), 1);
888
-
889
- return (
890
- <div className="p-6 space-y-6">
891
- <h1 className="text-2xl font-bold text-gray-900">${model} Analytics</h1>
892
-
893
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
894
- <div className="bg-white rounded-lg border border-gray-200 shadow-sm p-5">
895
- <h2 className="text-lg font-medium text-gray-900 mb-4">Status Distribution</h2>
896
- <div className="space-y-3">
897
- {Object.entries(statusCounts).map(([status, count]) => (
898
- <div key={status}>
899
- <div className="flex justify-between text-sm mb-1">
900
- <span className="text-gray-600">{status.replace(/[_-]/g, ' ')}</span>
901
- <span className="font-medium">{count}</span>
902
- </div>
903
- <div className="w-full bg-gray-100 rounded-full h-2">
904
- <div className="bg-blue-500 h-2 rounded-full transition-all" style={{ width: \`\${(count / maxCount) * 100}%\` }}></div>
905
- </div>
906
- </div>
907
- ))}
908
- </div>
909
- </div>
910
-
911
- ${numericAttrs.length > 0 ? ` <div className="bg-white rounded-lg border border-gray-200 shadow-sm p-5">
912
- <h2 className="text-lg font-medium text-gray-900 mb-4">Metrics</h2>
913
- <div className="space-y-4">
914
- ${numericAttrs.slice(0, 4).map(a => ` <div className="flex justify-between items-baseline">
915
- <span className="text-sm text-gray-600">${a.name.charAt(0).toUpperCase() + a.name.slice(1)}</span>
916
- <div className="text-right">
917
- <p className="text-lg font-bold text-gray-900">{items.reduce((s, i) => s + (Number(i.${a.name}) || 0), 0)}</p>
918
- <p className="text-xs text-gray-400">avg: {items.length ? (items.reduce((s, i) => s + (Number(i.${a.name}) || 0), 0) / items.length).toFixed(1) : 0}</p>
919
- </div>
920
- </div>`).join('\n')}
921
- </div>
922
- </div>` : ''}
923
- </div>
924
- </div>
925
- );
926
- }
927
-
928
- export default ${name};
929
- `;
930
- }
931
-
932
- // ============================================================================
933
- // Helpers
934
- // ============================================================================
935
-
936
- function capitalize(s: string): string {
937
- return s.charAt(0).toUpperCase() + s.slice(1);
938
- }