@atlashub/smartstack-cli 1.5.1 → 1.5.2
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.
- package/.documentation/css/styles.css +2168 -2168
- package/.documentation/js/app.js +794 -794
- package/config/default-config.json +86 -86
- package/config/settings.json +53 -53
- package/config/settings.local.example.json +16 -16
- package/dist/index.js +0 -0
- package/dist/index.js.map +1 -1
- package/package.json +88 -88
- package/templates/agents/action.md +36 -36
- package/templates/agents/efcore/conflicts.md +84 -84
- package/templates/agents/efcore/db-deploy.md +51 -51
- package/templates/agents/efcore/db-reset.md +59 -59
- package/templates/agents/efcore/db-seed.md +56 -56
- package/templates/agents/efcore/db-status.md +64 -64
- package/templates/agents/efcore/migration.md +85 -85
- package/templates/agents/efcore/rebase-snapshot.md +62 -62
- package/templates/agents/efcore/scan.md +90 -90
- package/templates/agents/efcore/squash.md +67 -67
- package/templates/agents/explore-codebase.md +65 -65
- package/templates/agents/explore-docs.md +97 -97
- package/templates/agents/fix-grammar.md +49 -49
- package/templates/agents/gitflow/abort.md +45 -45
- package/templates/agents/gitflow/cleanup.md +85 -85
- package/templates/agents/gitflow/commit.md +40 -40
- package/templates/agents/gitflow/exec.md +48 -48
- package/templates/agents/gitflow/finish.md +92 -92
- package/templates/agents/gitflow/init.md +139 -139
- package/templates/agents/gitflow/merge.md +62 -62
- package/templates/agents/gitflow/plan.md +42 -42
- package/templates/agents/gitflow/pr.md +78 -78
- package/templates/agents/gitflow/review.md +49 -49
- package/templates/agents/gitflow/start.md +61 -61
- package/templates/agents/gitflow/status.md +32 -32
- package/templates/agents/snipper.md +36 -36
- package/templates/agents/websearch.md +46 -46
- package/templates/commands/_resources/formatting-guide.md +124 -124
- package/templates/commands/ai-prompt.md +315 -315
- package/templates/commands/apex/1-analyze.md +100 -100
- package/templates/commands/apex/2-plan.md +145 -145
- package/templates/commands/apex/3-execute.md +171 -171
- package/templates/commands/apex/4-examine.md +116 -116
- package/templates/commands/apex/5-tasks.md +209 -209
- package/templates/commands/apex.md +76 -76
- package/templates/commands/application/create.md +362 -362
- package/templates/commands/application/templates-backend.md +463 -463
- package/templates/commands/application/templates-frontend.md +517 -517
- package/templates/commands/application/templates-i18n.md +478 -478
- package/templates/commands/application/templates-seed.md +362 -362
- package/templates/commands/application.md +303 -303
- package/templates/commands/business-analyse/0-orchestrate.md +640 -640
- package/templates/commands/business-analyse/1-init.md +269 -269
- package/templates/commands/business-analyse/2-discover.md +520 -520
- package/templates/commands/business-analyse/3-analyse.md +408 -408
- package/templates/commands/business-analyse/4-specify.md +598 -598
- package/templates/commands/business-analyse/5-validate.md +326 -326
- package/templates/commands/business-analyse/6-handoff.md +746 -746
- package/templates/commands/business-analyse/7-doc-html.md +602 -602
- package/templates/commands/business-analyse/bug.md +325 -325
- package/templates/commands/business-analyse/change-request.md +368 -368
- package/templates/commands/business-analyse/hotfix.md +200 -200
- package/templates/commands/business-analyse.md +640 -640
- package/templates/commands/controller/create.md +216 -216
- package/templates/commands/controller/postman-templates.md +528 -528
- package/templates/commands/controller/templates.md +600 -600
- package/templates/commands/controller.md +337 -337
- package/templates/commands/create/agent.md +138 -138
- package/templates/commands/create/command.md +166 -166
- package/templates/commands/create/hook.md +234 -234
- package/templates/commands/create/plugin.md +329 -329
- package/templates/commands/create/project.md +507 -507
- package/templates/commands/create/skill.md +199 -199
- package/templates/commands/create.md +220 -220
- package/templates/commands/debug.md +95 -95
- package/templates/commands/documentation/module.md +202 -202
- package/templates/commands/documentation/templates.md +432 -432
- package/templates/commands/documentation.md +190 -190
- package/templates/commands/efcore/_env-check.md +153 -153
- package/templates/commands/efcore/conflicts.md +186 -186
- package/templates/commands/efcore/db-deploy.md +193 -193
- package/templates/commands/efcore/db-reset.md +426 -426
- package/templates/commands/efcore/db-seed.md +326 -326
- package/templates/commands/efcore/db-status.md +226 -226
- package/templates/commands/efcore/migration.md +400 -400
- package/templates/commands/efcore/rebase-snapshot.md +264 -264
- package/templates/commands/efcore/scan.md +198 -198
- package/templates/commands/efcore/squash.md +298 -298
- package/templates/commands/efcore.md +224 -224
- package/templates/commands/epct.md +69 -69
- package/templates/commands/explain.md +186 -186
- package/templates/commands/explore.md +45 -45
- package/templates/commands/feature-full.md +267 -267
- package/templates/commands/gitflow/1-init.md +1038 -1038
- package/templates/commands/gitflow/10-start.md +768 -768
- package/templates/commands/gitflow/11-finish.md +457 -457
- package/templates/commands/gitflow/12-cleanup.md +276 -276
- package/templates/commands/gitflow/13-sync.md +216 -216
- package/templates/commands/gitflow/14-rebase.md +251 -251
- package/templates/commands/gitflow/2-status.md +277 -277
- package/templates/commands/gitflow/3-commit.md +344 -344
- package/templates/commands/gitflow/4-plan.md +145 -145
- package/templates/commands/gitflow/5-exec.md +147 -147
- package/templates/commands/gitflow/6-abort.md +344 -344
- package/templates/commands/gitflow/7-pull-request.md +453 -355
- package/templates/commands/gitflow/8-review.md +240 -176
- package/templates/commands/gitflow/9-merge.md +451 -365
- package/templates/commands/gitflow.md +128 -128
- package/templates/commands/implement.md +663 -663
- package/templates/commands/init.md +567 -567
- package/templates/commands/mcp-integration.md +330 -330
- package/templates/commands/notification.md +129 -129
- package/templates/commands/oneshot.md +57 -57
- package/templates/commands/quick-search.md +72 -72
- package/templates/commands/ralph-loop/cancel-ralph.md +18 -18
- package/templates/commands/ralph-loop/help.md +126 -126
- package/templates/commands/ralph-loop/ralph-loop.md +18 -18
- package/templates/commands/review.md +106 -106
- package/templates/commands/utils/test-web-config.md +160 -160
- package/templates/commands/utils/test-web.md +151 -151
- package/templates/commands/validate.md +233 -233
- package/templates/commands/workflow.md +193 -193
- package/templates/gitflow/config.json +138 -138
- package/templates/hooks/ef-migration-check.md +139 -139
- package/templates/hooks/hooks.json +25 -25
- package/templates/hooks/stop-hook.sh +177 -177
- package/templates/skills/ai-prompt/SKILL.md +778 -778
- package/templates/skills/application/SKILL.md +563 -563
- package/templates/skills/application/templates-backend.md +450 -450
- package/templates/skills/application/templates-frontend.md +531 -531
- package/templates/skills/application/templates-i18n.md +520 -520
- package/templates/skills/application/templates-seed.md +647 -647
- package/templates/skills/business-analyse/SKILL.md +191 -191
- package/templates/skills/business-analyse/questionnaire.md +283 -283
- package/templates/skills/business-analyse/templates-frd.md +477 -477
- package/templates/skills/business-analyse/templates-react.md +580 -580
- package/templates/skills/controller/SKILL.md +240 -240
- package/templates/skills/controller/postman-templates.md +614 -614
- package/templates/skills/controller/templates.md +1468 -1468
- package/templates/skills/documentation/SKILL.md +133 -133
- package/templates/skills/documentation/templates.md +476 -476
- package/templates/skills/feature-full/SKILL.md +838 -838
- package/templates/skills/notification/SKILL.md +555 -555
- package/templates/skills/ui-components/SKILL.md +870 -870
- package/templates/skills/workflow/SKILL.md +582 -582
- package/templates/test-web/api-health.json +38 -38
- package/templates/test-web/minimal.json +19 -19
- package/templates/test-web/npm-package.json +46 -46
- package/templates/test-web/seo-check.json +54 -54
|
@@ -1,531 +1,531 @@
|
|
|
1
|
-
# Templates Frontend - Application Skill
|
|
2
|
-
|
|
3
|
-
> Ces templates génèrent le code React/TypeScript pour les nouvelles applications/modules.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## ARCHITECTURE FRONTEND
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
web/smartstack-web/src/
|
|
11
|
-
├── pages/$CONTEXT/$MODULE/
|
|
12
|
-
│ ├── $MODULE_PASCALPage.tsx # Page principale (liste)
|
|
13
|
-
│ ├── $MODULE_PASCALDetailPage.tsx # Page détail
|
|
14
|
-
│ └── Create$MODULE_PASCALPage.tsx # Page création
|
|
15
|
-
├── components/$MODULE/
|
|
16
|
-
│ ├── $MODULE_PASCALListView.tsx # Composant liste réutilisable
|
|
17
|
-
│ ├── $MODULE_PASCALForm.tsx # Formulaire CRUD
|
|
18
|
-
│ └── $MODULE_PASCALFilters.tsx # Filtres
|
|
19
|
-
├── components/common/
|
|
20
|
-
│ └── (composants partagés)
|
|
21
|
-
├── hooks/
|
|
22
|
-
│ ├── use$MODULE_PASCALPreferences.ts # Hook préférences
|
|
23
|
-
│ └── use$MODULE_PASCAL.ts # Hook API
|
|
24
|
-
├── services/api/
|
|
25
|
-
│ └── $moduleApi.ts # Service API
|
|
26
|
-
└── i18n/locales/
|
|
27
|
-
├── fr/$module.json
|
|
28
|
-
├── en/$module.json
|
|
29
|
-
├── it/$module.json
|
|
30
|
-
└── de/$module.json
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## TEMPLATE: PAGE PRINCIPALE
|
|
36
|
-
|
|
37
|
-
```tsx
|
|
38
|
-
// pages/$CONTEXT/$MODULE/$MODULE_PASCALPage.tsx
|
|
39
|
-
|
|
40
|
-
import { useTranslation } from 'react-i18next';
|
|
41
|
-
import { $MODULE_PASCALListView } from '@/components/$MODULE/$MODULE_PASCALListView';
|
|
42
|
-
|
|
43
|
-
export function $MODULE_PASCALPage() {
|
|
44
|
-
const { t } = useTranslation(['$module', 'common']);
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<$MODULE_PASCALListView
|
|
48
|
-
title={t('$module:title')}
|
|
49
|
-
subtitle={t('$module:subtitle')}
|
|
50
|
-
basePath="/$CONTEXT/$APPLICATION/$MODULE"
|
|
51
|
-
createPath="/$CONTEXT/$APPLICATION/$MODULE/new"
|
|
52
|
-
/>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## TEMPLATE: LISTE VIEW (Composant réutilisable)
|
|
60
|
-
|
|
61
|
-
```tsx
|
|
62
|
-
// components/$MODULE/$MODULE_PASCALListView.tsx
|
|
63
|
-
|
|
64
|
-
import { useState, useEffect } from 'react';
|
|
65
|
-
import { useNavigate } from 'react-router-dom';
|
|
66
|
-
import { useTranslation } from 'react-i18next';
|
|
67
|
-
import { Plus, Search, MoreVertical, Pencil, Trash2, Check } from 'lucide-react';
|
|
68
|
-
import { api } from '@/services/api/apiClient';
|
|
69
|
-
import { use$MODULE_PASCALPreferences } from '@/hooks/use$MODULE_PASCALPreferences';
|
|
70
|
-
import { Pagination } from '@/components/common/Pagination';
|
|
71
|
-
import { ColumnSelector } from '@/components/common/ColumnSelector';
|
|
72
|
-
import { ViewModeToggle } from '@/components/common/ViewModeToggle';
|
|
73
|
-
import { EntityCard } from '@/components/ui/EntityCard'; // ⚠️ OBLIGATOIRE pour Grid view
|
|
74
|
-
|
|
75
|
-
interface $ENTITY_PASCALDto {
|
|
76
|
-
id: string;
|
|
77
|
-
name: string;
|
|
78
|
-
description: string | null;
|
|
79
|
-
isActive: boolean;
|
|
80
|
-
createdAt: string;
|
|
81
|
-
updatedAt: string | null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
interface PagedResult {
|
|
85
|
-
items: $ENTITY_PASCALDto[];
|
|
86
|
-
totalCount: number;
|
|
87
|
-
page: number;
|
|
88
|
-
pageSize: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
interface $MODULE_PASCALListViewProps {
|
|
92
|
-
title: string;
|
|
93
|
-
subtitle?: string;
|
|
94
|
-
basePath: string;
|
|
95
|
-
createPath?: string;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const DEFAULT_COLUMNS = ['name', 'description', 'isActive', 'createdAt'];
|
|
99
|
-
|
|
100
|
-
export function $MODULE_PASCALListView({
|
|
101
|
-
title,
|
|
102
|
-
subtitle,
|
|
103
|
-
basePath,
|
|
104
|
-
createPath,
|
|
105
|
-
}: $MODULE_PASCALListViewProps) {
|
|
106
|
-
const { t } = useTranslation(['$module', 'common']);
|
|
107
|
-
const navigate = useNavigate();
|
|
108
|
-
const {
|
|
109
|
-
pageSize,
|
|
110
|
-
sortColumn,
|
|
111
|
-
sortDirection,
|
|
112
|
-
visibleColumns,
|
|
113
|
-
viewMode,
|
|
114
|
-
setPageSize,
|
|
115
|
-
setSortColumn,
|
|
116
|
-
setSortDirection,
|
|
117
|
-
setVisibleColumns,
|
|
118
|
-
setViewMode,
|
|
119
|
-
} = use$MODULE_PASCALPreferences();
|
|
120
|
-
|
|
121
|
-
const [data, setData] = useState<PagedResult | null>(null);
|
|
122
|
-
const [loading, setLoading] = useState(true);
|
|
123
|
-
const [error, setError] = useState<string | null>(null);
|
|
124
|
-
const [page, setPage] = useState(1);
|
|
125
|
-
const [searchTerm, setSearchTerm] = useState('');
|
|
126
|
-
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
loadData();
|
|
129
|
-
}, [page, pageSize, sortColumn, sortDirection, searchTerm]);
|
|
130
|
-
|
|
131
|
-
const loadData = async () => {
|
|
132
|
-
try {
|
|
133
|
-
setLoading(true);
|
|
134
|
-
setError(null);
|
|
135
|
-
const params = new URLSearchParams({
|
|
136
|
-
page: page.toString(),
|
|
137
|
-
pageSize: pageSize.toString(),
|
|
138
|
-
sortColumn,
|
|
139
|
-
sortDirection,
|
|
140
|
-
...(searchTerm && { searchTerm }),
|
|
141
|
-
});
|
|
142
|
-
const result = await api.get<PagedResult>(`/api/$module?${params}`);
|
|
143
|
-
setData(result);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
setError(t('common:errors.loadFailed'));
|
|
146
|
-
console.error('Failed to load $module:', err);
|
|
147
|
-
} finally {
|
|
148
|
-
setLoading(false);
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const handleSort = (column: string) => {
|
|
153
|
-
if (sortColumn === column) {
|
|
154
|
-
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
|
155
|
-
} else {
|
|
156
|
-
setSortColumn(column);
|
|
157
|
-
setSortDirection('asc');
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const handleDelete = async (id: string) => {
|
|
162
|
-
if (!confirm(t('common:confirmDelete'))) return;
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
await api.delete(`/api/$module/${id}`);
|
|
166
|
-
loadData();
|
|
167
|
-
} catch (err) {
|
|
168
|
-
console.error('Failed to delete:', err);
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const columns = [
|
|
173
|
-
{ key: 'name', label: t('$module:columns.name'), sortable: true },
|
|
174
|
-
{ key: 'description', label: t('$module:columns.description'), sortable: false },
|
|
175
|
-
{ key: 'isActive', label: t('$module:columns.status'), sortable: true },
|
|
176
|
-
{ key: 'createdAt', label: t('$module:columns.createdAt'), sortable: true },
|
|
177
|
-
];
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<div className="space-y-6">
|
|
181
|
-
{/* Header */}
|
|
182
|
-
<div className="flex items-center justify-between">
|
|
183
|
-
<div>
|
|
184
|
-
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{title}</h1>
|
|
185
|
-
{subtitle && (
|
|
186
|
-
<p className="text-[var(--text-secondary)] mt-1">{subtitle}</p>
|
|
187
|
-
)}
|
|
188
|
-
</div>
|
|
189
|
-
{createPath && (
|
|
190
|
-
<button
|
|
191
|
-
onClick={() => navigate(createPath)}
|
|
192
|
-
className="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent-500)] hover:bg-[var(--color-accent-600)] text-white font-medium transition-colors"
|
|
193
|
-
style={{ borderRadius: 'var(--radius-button)' }}
|
|
194
|
-
>
|
|
195
|
-
<Plus className="w-4 h-4" />
|
|
196
|
-
{t('common:actions.create')}
|
|
197
|
-
</button>
|
|
198
|
-
)}
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
{/* Toolbar */}
|
|
202
|
-
<div className="flex items-center justify-between gap-4">
|
|
203
|
-
<div className="relative flex-1 max-w-md">
|
|
204
|
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)]" />
|
|
205
|
-
<input
|
|
206
|
-
type="text"
|
|
207
|
-
placeholder={t('common:search.placeholder')}
|
|
208
|
-
value={searchTerm}
|
|
209
|
-
onChange={(e) => setSearchTerm(e.target.value)}
|
|
210
|
-
className="w-full pl-10 pr-4 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] text-sm focus:outline-none focus:border-[var(--color-accent-500)]"
|
|
211
|
-
style={{ borderRadius: 'var(--radius-input)' }}
|
|
212
|
-
/>
|
|
213
|
-
</div>
|
|
214
|
-
<div className="flex items-center gap-2">
|
|
215
|
-
<ColumnSelector
|
|
216
|
-
columns={columns}
|
|
217
|
-
visibleColumns={visibleColumns}
|
|
218
|
-
onChange={setVisibleColumns}
|
|
219
|
-
/>
|
|
220
|
-
<ViewModeToggle value={viewMode} onChange={setViewMode} />
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
{/* Content */}
|
|
225
|
-
{loading ? (
|
|
226
|
-
<div className="flex items-center justify-center py-12">
|
|
227
|
-
<div className="animate-spin h-8 w-8 border-4 border-[var(--color-accent-500)] border-t-transparent rounded-full" />
|
|
228
|
-
</div>
|
|
229
|
-
) : error ? (
|
|
230
|
-
<div className="p-4 bg-[var(--error-bg)] border border-[var(--error-border)] text-[var(--error-text)]" style={{ borderRadius: 'var(--radius-card)' }}>
|
|
231
|
-
{error}
|
|
232
|
-
</div>
|
|
233
|
-
) : viewMode === 'list' ? (
|
|
234
|
-
<div className="bg-[var(--bg-card)] border border-[var(--item-color-border)] overflow-hidden" style={{ borderRadius: 'var(--radius-card)' }}>
|
|
235
|
-
<table className="w-full">
|
|
236
|
-
<thead>
|
|
237
|
-
<tr className="border-b border-[var(--item-color-border)]">
|
|
238
|
-
{columns.filter(col => visibleColumns.includes(col.key)).map((col) => (
|
|
239
|
-
<th
|
|
240
|
-
key={col.key}
|
|
241
|
-
className={`px-4 py-3 text-left text-sm font-medium text-[var(--text-secondary)] ${
|
|
242
|
-
col.sortable ? 'cursor-pointer hover:text-[var(--text-primary)]' : ''
|
|
243
|
-
}`}
|
|
244
|
-
onClick={() => col.sortable && handleSort(col.key)}
|
|
245
|
-
>
|
|
246
|
-
{col.label}
|
|
247
|
-
{sortColumn === col.key && (
|
|
248
|
-
<span className="ml-1">{sortDirection === 'asc' ? '↑' : '↓'}</span>
|
|
249
|
-
)}
|
|
250
|
-
</th>
|
|
251
|
-
))}
|
|
252
|
-
<th className="px-4 py-3 text-right text-sm font-medium text-[var(--text-secondary)]">
|
|
253
|
-
{t('common:actions.title')}
|
|
254
|
-
</th>
|
|
255
|
-
</tr>
|
|
256
|
-
</thead>
|
|
257
|
-
<tbody>
|
|
258
|
-
{data?.items.map((item) => (
|
|
259
|
-
<tr
|
|
260
|
-
key={item.id}
|
|
261
|
-
className="border-b border-[var(--item-color-border)] hover:bg-[var(--bg-hover)] cursor-pointer"
|
|
262
|
-
onClick={() => navigate(`${basePath}/${item.id}`)}
|
|
263
|
-
>
|
|
264
|
-
{visibleColumns.includes('name') && (
|
|
265
|
-
<td className="px-4 py-3 text-sm text-[var(--text-primary)] font-medium">
|
|
266
|
-
{item.name}
|
|
267
|
-
</td>
|
|
268
|
-
)}
|
|
269
|
-
{visibleColumns.includes('description') && (
|
|
270
|
-
<td className="px-4 py-3 text-sm text-[var(--text-secondary)]">
|
|
271
|
-
{item.description || '-'}
|
|
272
|
-
</td>
|
|
273
|
-
)}
|
|
274
|
-
{visibleColumns.includes('isActive') && (
|
|
275
|
-
<td className="px-4 py-3">
|
|
276
|
-
<span
|
|
277
|
-
className={`inline-flex items-center px-2 py-1 text-xs font-medium ${
|
|
278
|
-
item.isActive
|
|
279
|
-
? 'bg-green-100 text-green-800'
|
|
280
|
-
: 'bg-gray-100 text-gray-800'
|
|
281
|
-
}`}
|
|
282
|
-
style={{ borderRadius: 'var(--radius-badge)' }}
|
|
283
|
-
>
|
|
284
|
-
{item.isActive ? t('common:status.active') : t('common:status.inactive')}
|
|
285
|
-
</span>
|
|
286
|
-
</td>
|
|
287
|
-
)}
|
|
288
|
-
{visibleColumns.includes('createdAt') && (
|
|
289
|
-
<td className="px-4 py-3 text-sm text-[var(--text-secondary)]">
|
|
290
|
-
{new Date(item.createdAt).toLocaleDateString()}
|
|
291
|
-
</td>
|
|
292
|
-
)}
|
|
293
|
-
<td className="px-4 py-3 text-right">
|
|
294
|
-
<div className="flex items-center justify-end gap-2">
|
|
295
|
-
<button
|
|
296
|
-
onClick={(e) => {
|
|
297
|
-
e.stopPropagation();
|
|
298
|
-
navigate(`${basePath}/${item.id}/edit`);
|
|
299
|
-
}}
|
|
300
|
-
className="p-1 hover:bg-[var(--bg-tertiary)]"
|
|
301
|
-
style={{ borderRadius: 'var(--radius-button)' }}
|
|
302
|
-
>
|
|
303
|
-
<Pencil className="w-4 h-4 text-[var(--text-secondary)]" />
|
|
304
|
-
</button>
|
|
305
|
-
<button
|
|
306
|
-
onClick={(e) => {
|
|
307
|
-
e.stopPropagation();
|
|
308
|
-
handleDelete(item.id);
|
|
309
|
-
}}
|
|
310
|
-
className="p-1 hover:bg-[var(--error-bg)]"
|
|
311
|
-
style={{ borderRadius: 'var(--radius-button)' }}
|
|
312
|
-
>
|
|
313
|
-
<Trash2 className="w-4 h-4 text-[var(--error-text)]" />
|
|
314
|
-
</button>
|
|
315
|
-
</div>
|
|
316
|
-
</td>
|
|
317
|
-
</tr>
|
|
318
|
-
))}
|
|
319
|
-
</tbody>
|
|
320
|
-
</table>
|
|
321
|
-
</div>
|
|
322
|
-
) : (
|
|
323
|
-
/* Grid view - ⚠️ OBLIGATOIRE: utiliser EntityCard */
|
|
324
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
325
|
-
{data?.items.map((item) => (
|
|
326
|
-
<EntityCard
|
|
327
|
-
key={item.id}
|
|
328
|
-
avatar={{ letter: item.name[0].toUpperCase(), color: 'var(--color-accent-500)' }}
|
|
329
|
-
title={item.name}
|
|
330
|
-
subtitle={item.code}
|
|
331
|
-
description={item.description}
|
|
332
|
-
badge={item.isActive ? {
|
|
333
|
-
icon: Check,
|
|
334
|
-
tooltip: t('common:status.active'),
|
|
335
|
-
color: 'var(--success-text)'
|
|
336
|
-
} : undefined}
|
|
337
|
-
stats={`${t('common:createdAt')}: ${new Date(item.createdAt).toLocaleDateString()}`}
|
|
338
|
-
onClick={() => navigate(`${basePath}/${item.id}`)}
|
|
339
|
-
actions={[
|
|
340
|
-
{ label: t('common:actions.view'), onClick: () => navigate(`${basePath}/${item.id}`), variant: 'primary' },
|
|
341
|
-
]}
|
|
342
|
-
/>
|
|
343
|
-
))}
|
|
344
|
-
</div>
|
|
345
|
-
)}
|
|
346
|
-
|
|
347
|
-
{/* Pagination */}
|
|
348
|
-
{data && (
|
|
349
|
-
<Pagination
|
|
350
|
-
page={data.page}
|
|
351
|
-
pageSize={data.pageSize}
|
|
352
|
-
totalCount={data.totalCount}
|
|
353
|
-
onPageChange={setPage}
|
|
354
|
-
onPageSizeChange={setPageSize}
|
|
355
|
-
/>
|
|
356
|
-
)}
|
|
357
|
-
</div>
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
---
|
|
363
|
-
|
|
364
|
-
## TEMPLATE: HOOK PRÉFÉRENCES
|
|
365
|
-
|
|
366
|
-
```tsx
|
|
367
|
-
// hooks/use$MODULE_PASCALPreferences.ts
|
|
368
|
-
|
|
369
|
-
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
|
|
370
|
-
|
|
371
|
-
const DEFAULT_COLUMNS = ['name', 'description', 'isActive', 'createdAt'];
|
|
372
|
-
|
|
373
|
-
export function use$MODULE_PASCALPreferences() {
|
|
374
|
-
const { preferences, updatePreference } = useUserPreferences();
|
|
375
|
-
const modulePrefs = preferences.$module || {};
|
|
376
|
-
|
|
377
|
-
return {
|
|
378
|
-
// Getters with defaults
|
|
379
|
-
pageSize: modulePrefs.pageSize ?? 10,
|
|
380
|
-
sortColumn: modulePrefs.sortColumn ?? 'createdAt',
|
|
381
|
-
sortDirection: modulePrefs.sortDirection ?? 'desc',
|
|
382
|
-
filters: modulePrefs.filters ?? {},
|
|
383
|
-
visibleColumns: modulePrefs.visibleColumns ?? DEFAULT_COLUMNS,
|
|
384
|
-
viewMode: modulePrefs.viewMode ?? 'list',
|
|
385
|
-
|
|
386
|
-
// Setters
|
|
387
|
-
setPageSize: (size: number) =>
|
|
388
|
-
updatePreference('$module.pageSize', size),
|
|
389
|
-
setSortColumn: (col: string) =>
|
|
390
|
-
updatePreference('$module.sortColumn', col),
|
|
391
|
-
setSortDirection: (dir: 'asc' | 'desc') =>
|
|
392
|
-
updatePreference('$module.sortDirection', dir),
|
|
393
|
-
setFilters: (filters: Record<string, unknown>) =>
|
|
394
|
-
updatePreference('$module.filters', filters),
|
|
395
|
-
setVisibleColumns: (cols: string[]) =>
|
|
396
|
-
updatePreference('$module.visibleColumns', cols),
|
|
397
|
-
setViewMode: (mode: 'list' | 'grid') =>
|
|
398
|
-
updatePreference('$module.viewMode', mode),
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
---
|
|
404
|
-
|
|
405
|
-
## TEMPLATE: SERVICE API
|
|
406
|
-
|
|
407
|
-
```tsx
|
|
408
|
-
// services/api/$moduleApi.ts
|
|
409
|
-
|
|
410
|
-
import { api } from './apiClient';
|
|
411
|
-
|
|
412
|
-
export interface $ENTITY_PASCALDto {
|
|
413
|
-
id: string;
|
|
414
|
-
name: string;
|
|
415
|
-
description: string | null;
|
|
416
|
-
isActive: boolean;
|
|
417
|
-
createdAt: string;
|
|
418
|
-
updatedAt: string | null;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
export interface Create$ENTITY_PASCALRequest {
|
|
422
|
-
name: string;
|
|
423
|
-
description?: string;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export interface Update$ENTITY_PASCALRequest {
|
|
427
|
-
name: string;
|
|
428
|
-
description?: string;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export interface PagedResult<T> {
|
|
432
|
-
items: T[];
|
|
433
|
-
totalCount: number;
|
|
434
|
-
page: number;
|
|
435
|
-
pageSize: number;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
export interface QueryParameters {
|
|
439
|
-
page?: number;
|
|
440
|
-
pageSize?: number;
|
|
441
|
-
searchTerm?: string;
|
|
442
|
-
sortColumn?: string;
|
|
443
|
-
sortDirection?: 'asc' | 'desc';
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
export const $moduleApi = {
|
|
447
|
-
getAll: async (params: QueryParameters = {}): Promise<PagedResult<$ENTITY_PASCALDto>> => {
|
|
448
|
-
const queryParams = new URLSearchParams();
|
|
449
|
-
if (params.page) queryParams.set('page', params.page.toString());
|
|
450
|
-
if (params.pageSize) queryParams.set('pageSize', params.pageSize.toString());
|
|
451
|
-
if (params.searchTerm) queryParams.set('searchTerm', params.searchTerm);
|
|
452
|
-
if (params.sortColumn) queryParams.set('sortColumn', params.sortColumn);
|
|
453
|
-
if (params.sortDirection) queryParams.set('sortDirection', params.sortDirection);
|
|
454
|
-
|
|
455
|
-
return api.get(`/api/$module?${queryParams}`);
|
|
456
|
-
},
|
|
457
|
-
|
|
458
|
-
getById: async (id: string): Promise<$ENTITY_PASCALDto> => {
|
|
459
|
-
return api.get(`/api/$module/${id}`);
|
|
460
|
-
},
|
|
461
|
-
|
|
462
|
-
create: async (data: Create$ENTITY_PASCALRequest): Promise<$ENTITY_PASCALDto> => {
|
|
463
|
-
return api.post('/api/$module', data);
|
|
464
|
-
},
|
|
465
|
-
|
|
466
|
-
update: async (id: string, data: Update$ENTITY_PASCALRequest): Promise<$ENTITY_PASCALDto> => {
|
|
467
|
-
return api.put(`/api/$module/${id}`, data);
|
|
468
|
-
},
|
|
469
|
-
|
|
470
|
-
delete: async (id: string): Promise<void> => {
|
|
471
|
-
return api.delete(`/api/$module/${id}`);
|
|
472
|
-
},
|
|
473
|
-
};
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
---
|
|
477
|
-
|
|
478
|
-
## TEMPLATE: ROUTES (App.tsx)
|
|
479
|
-
|
|
480
|
-
### ⚠️ RÈGLE CRITIQUE: ROUTES IMBRIQUÉES OBLIGATOIRES
|
|
481
|
-
|
|
482
|
-
React Router v7 **exige** des routes imbriquées (nested routes) pour les applications multi-modules.
|
|
483
|
-
|
|
484
|
-
**❌ INTERDIT - Routes plates (causent des redirections vers Home)**
|
|
485
|
-
```tsx
|
|
486
|
-
<Route path="$APPLICATION" element={<Navigate to="/$CONTEXT/$APPLICATION/$DEFAULT_MODULE" replace />} />
|
|
487
|
-
<Route path="$APPLICATION/$MODULE" element={<$MODULE_PASCALPage />} />
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
**✅ OBLIGATOIRE - Routes imbriquées avec index**
|
|
491
|
-
```tsx
|
|
492
|
-
// Ajouter dans App.tsx
|
|
493
|
-
|
|
494
|
-
import { $MODULE_PASCALPage } from '@/pages/$CONTEXT/$MODULE/$MODULE_PASCALPage';
|
|
495
|
-
import { $MODULE_PASCALDetailPage } from '@/pages/$CONTEXT/$MODULE/$MODULE_PASCALDetailPage';
|
|
496
|
-
import { Create$MODULE_PASCALPage } from '@/pages/$CONTEXT/$MODULE/Create$MODULE_PASCALPage';
|
|
497
|
-
|
|
498
|
-
// Dans les routes - STRUCTURE IMBRIQUÉE
|
|
499
|
-
<Route path="$APPLICATION">
|
|
500
|
-
<Route index element={<Navigate to="$DEFAULT_MODULE" replace />} />
|
|
501
|
-
<Route path="$MODULE" element={<$MODULE_PASCALPage />} />
|
|
502
|
-
<Route path="$MODULE/new" element={<Create$MODULE_PASCALPage />} />
|
|
503
|
-
<Route path="$MODULE/:id" element={<$MODULE_PASCALDetailPage />} />
|
|
504
|
-
<Route path="$MODULE/:id/edit" element={<Create$MODULE_PASCALPage />} />
|
|
505
|
-
</Route>
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
### Pourquoi cette structure ?
|
|
509
|
-
|
|
510
|
-
| Aspect | Routes plates | Routes imbriquées |
|
|
511
|
-
|--------|--------------|-------------------|
|
|
512
|
-
| Matching | Ambigu entre siblings | Hiérarchique clair |
|
|
513
|
-
| Navigate | Doit être absolu | Peut être relatif |
|
|
514
|
-
| Outlet | Non supporté | Supporté |
|
|
515
|
-
| Redirect | Peut échouer | Fonctionne toujours |
|
|
516
|
-
|
|
517
|
-
---
|
|
518
|
-
|
|
519
|
-
## CHECKLIST FRONTEND
|
|
520
|
-
|
|
521
|
-
| Vérification | Status |
|
|
522
|
-
|--------------|--------|
|
|
523
|
-
| ☐ Page principale créée | |
|
|
524
|
-
| ☐ Composant ListView créé | |
|
|
525
|
-
| ☐ Hook préférences créé | |
|
|
526
|
-
| ☐ Service API créé | |
|
|
527
|
-
| ☐ Routes ajoutées dans App.tsx | |
|
|
528
|
-
| ☐ Utilise apiClient (pas d'appel direct) | |
|
|
529
|
-
| ☐ Pas d'import Infrastructure | |
|
|
530
|
-
| ☐ npm run build réussi | |
|
|
531
|
-
| ☐ npm run lint réussi | |
|
|
1
|
+
# Templates Frontend - Application Skill
|
|
2
|
+
|
|
3
|
+
> Ces templates génèrent le code React/TypeScript pour les nouvelles applications/modules.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ARCHITECTURE FRONTEND
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
web/smartstack-web/src/
|
|
11
|
+
├── pages/$CONTEXT/$MODULE/
|
|
12
|
+
│ ├── $MODULE_PASCALPage.tsx # Page principale (liste)
|
|
13
|
+
│ ├── $MODULE_PASCALDetailPage.tsx # Page détail
|
|
14
|
+
│ └── Create$MODULE_PASCALPage.tsx # Page création
|
|
15
|
+
├── components/$MODULE/
|
|
16
|
+
│ ├── $MODULE_PASCALListView.tsx # Composant liste réutilisable
|
|
17
|
+
│ ├── $MODULE_PASCALForm.tsx # Formulaire CRUD
|
|
18
|
+
│ └── $MODULE_PASCALFilters.tsx # Filtres
|
|
19
|
+
├── components/common/
|
|
20
|
+
│ └── (composants partagés)
|
|
21
|
+
├── hooks/
|
|
22
|
+
│ ├── use$MODULE_PASCALPreferences.ts # Hook préférences
|
|
23
|
+
│ └── use$MODULE_PASCAL.ts # Hook API
|
|
24
|
+
├── services/api/
|
|
25
|
+
│ └── $moduleApi.ts # Service API
|
|
26
|
+
└── i18n/locales/
|
|
27
|
+
├── fr/$module.json
|
|
28
|
+
├── en/$module.json
|
|
29
|
+
├── it/$module.json
|
|
30
|
+
└── de/$module.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## TEMPLATE: PAGE PRINCIPALE
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// pages/$CONTEXT/$MODULE/$MODULE_PASCALPage.tsx
|
|
39
|
+
|
|
40
|
+
import { useTranslation } from 'react-i18next';
|
|
41
|
+
import { $MODULE_PASCALListView } from '@/components/$MODULE/$MODULE_PASCALListView';
|
|
42
|
+
|
|
43
|
+
export function $MODULE_PASCALPage() {
|
|
44
|
+
const { t } = useTranslation(['$module', 'common']);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<$MODULE_PASCALListView
|
|
48
|
+
title={t('$module:title')}
|
|
49
|
+
subtitle={t('$module:subtitle')}
|
|
50
|
+
basePath="/$CONTEXT/$APPLICATION/$MODULE"
|
|
51
|
+
createPath="/$CONTEXT/$APPLICATION/$MODULE/new"
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## TEMPLATE: LISTE VIEW (Composant réutilisable)
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// components/$MODULE/$MODULE_PASCALListView.tsx
|
|
63
|
+
|
|
64
|
+
import { useState, useEffect } from 'react';
|
|
65
|
+
import { useNavigate } from 'react-router-dom';
|
|
66
|
+
import { useTranslation } from 'react-i18next';
|
|
67
|
+
import { Plus, Search, MoreVertical, Pencil, Trash2, Check } from 'lucide-react';
|
|
68
|
+
import { api } from '@/services/api/apiClient';
|
|
69
|
+
import { use$MODULE_PASCALPreferences } from '@/hooks/use$MODULE_PASCALPreferences';
|
|
70
|
+
import { Pagination } from '@/components/common/Pagination';
|
|
71
|
+
import { ColumnSelector } from '@/components/common/ColumnSelector';
|
|
72
|
+
import { ViewModeToggle } from '@/components/common/ViewModeToggle';
|
|
73
|
+
import { EntityCard } from '@/components/ui/EntityCard'; // ⚠️ OBLIGATOIRE pour Grid view
|
|
74
|
+
|
|
75
|
+
interface $ENTITY_PASCALDto {
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
description: string | null;
|
|
79
|
+
isActive: boolean;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
updatedAt: string | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface PagedResult {
|
|
85
|
+
items: $ENTITY_PASCALDto[];
|
|
86
|
+
totalCount: number;
|
|
87
|
+
page: number;
|
|
88
|
+
pageSize: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface $MODULE_PASCALListViewProps {
|
|
92
|
+
title: string;
|
|
93
|
+
subtitle?: string;
|
|
94
|
+
basePath: string;
|
|
95
|
+
createPath?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DEFAULT_COLUMNS = ['name', 'description', 'isActive', 'createdAt'];
|
|
99
|
+
|
|
100
|
+
export function $MODULE_PASCALListView({
|
|
101
|
+
title,
|
|
102
|
+
subtitle,
|
|
103
|
+
basePath,
|
|
104
|
+
createPath,
|
|
105
|
+
}: $MODULE_PASCALListViewProps) {
|
|
106
|
+
const { t } = useTranslation(['$module', 'common']);
|
|
107
|
+
const navigate = useNavigate();
|
|
108
|
+
const {
|
|
109
|
+
pageSize,
|
|
110
|
+
sortColumn,
|
|
111
|
+
sortDirection,
|
|
112
|
+
visibleColumns,
|
|
113
|
+
viewMode,
|
|
114
|
+
setPageSize,
|
|
115
|
+
setSortColumn,
|
|
116
|
+
setSortDirection,
|
|
117
|
+
setVisibleColumns,
|
|
118
|
+
setViewMode,
|
|
119
|
+
} = use$MODULE_PASCALPreferences();
|
|
120
|
+
|
|
121
|
+
const [data, setData] = useState<PagedResult | null>(null);
|
|
122
|
+
const [loading, setLoading] = useState(true);
|
|
123
|
+
const [error, setError] = useState<string | null>(null);
|
|
124
|
+
const [page, setPage] = useState(1);
|
|
125
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
loadData();
|
|
129
|
+
}, [page, pageSize, sortColumn, sortDirection, searchTerm]);
|
|
130
|
+
|
|
131
|
+
const loadData = async () => {
|
|
132
|
+
try {
|
|
133
|
+
setLoading(true);
|
|
134
|
+
setError(null);
|
|
135
|
+
const params = new URLSearchParams({
|
|
136
|
+
page: page.toString(),
|
|
137
|
+
pageSize: pageSize.toString(),
|
|
138
|
+
sortColumn,
|
|
139
|
+
sortDirection,
|
|
140
|
+
...(searchTerm && { searchTerm }),
|
|
141
|
+
});
|
|
142
|
+
const result = await api.get<PagedResult>(`/api/$module?${params}`);
|
|
143
|
+
setData(result);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
setError(t('common:errors.loadFailed'));
|
|
146
|
+
console.error('Failed to load $module:', err);
|
|
147
|
+
} finally {
|
|
148
|
+
setLoading(false);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleSort = (column: string) => {
|
|
153
|
+
if (sortColumn === column) {
|
|
154
|
+
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
|
155
|
+
} else {
|
|
156
|
+
setSortColumn(column);
|
|
157
|
+
setSortDirection('asc');
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const handleDelete = async (id: string) => {
|
|
162
|
+
if (!confirm(t('common:confirmDelete'))) return;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await api.delete(`/api/$module/${id}`);
|
|
166
|
+
loadData();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error('Failed to delete:', err);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const columns = [
|
|
173
|
+
{ key: 'name', label: t('$module:columns.name'), sortable: true },
|
|
174
|
+
{ key: 'description', label: t('$module:columns.description'), sortable: false },
|
|
175
|
+
{ key: 'isActive', label: t('$module:columns.status'), sortable: true },
|
|
176
|
+
{ key: 'createdAt', label: t('$module:columns.createdAt'), sortable: true },
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className="space-y-6">
|
|
181
|
+
{/* Header */}
|
|
182
|
+
<div className="flex items-center justify-between">
|
|
183
|
+
<div>
|
|
184
|
+
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{title}</h1>
|
|
185
|
+
{subtitle && (
|
|
186
|
+
<p className="text-[var(--text-secondary)] mt-1">{subtitle}</p>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
{createPath && (
|
|
190
|
+
<button
|
|
191
|
+
onClick={() => navigate(createPath)}
|
|
192
|
+
className="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent-500)] hover:bg-[var(--color-accent-600)] text-white font-medium transition-colors"
|
|
193
|
+
style={{ borderRadius: 'var(--radius-button)' }}
|
|
194
|
+
>
|
|
195
|
+
<Plus className="w-4 h-4" />
|
|
196
|
+
{t('common:actions.create')}
|
|
197
|
+
</button>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Toolbar */}
|
|
202
|
+
<div className="flex items-center justify-between gap-4">
|
|
203
|
+
<div className="relative flex-1 max-w-md">
|
|
204
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)]" />
|
|
205
|
+
<input
|
|
206
|
+
type="text"
|
|
207
|
+
placeholder={t('common:search.placeholder')}
|
|
208
|
+
value={searchTerm}
|
|
209
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
210
|
+
className="w-full pl-10 pr-4 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] text-sm focus:outline-none focus:border-[var(--color-accent-500)]"
|
|
211
|
+
style={{ borderRadius: 'var(--radius-input)' }}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
<div className="flex items-center gap-2">
|
|
215
|
+
<ColumnSelector
|
|
216
|
+
columns={columns}
|
|
217
|
+
visibleColumns={visibleColumns}
|
|
218
|
+
onChange={setVisibleColumns}
|
|
219
|
+
/>
|
|
220
|
+
<ViewModeToggle value={viewMode} onChange={setViewMode} />
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{/* Content */}
|
|
225
|
+
{loading ? (
|
|
226
|
+
<div className="flex items-center justify-center py-12">
|
|
227
|
+
<div className="animate-spin h-8 w-8 border-4 border-[var(--color-accent-500)] border-t-transparent rounded-full" />
|
|
228
|
+
</div>
|
|
229
|
+
) : error ? (
|
|
230
|
+
<div className="p-4 bg-[var(--error-bg)] border border-[var(--error-border)] text-[var(--error-text)]" style={{ borderRadius: 'var(--radius-card)' }}>
|
|
231
|
+
{error}
|
|
232
|
+
</div>
|
|
233
|
+
) : viewMode === 'list' ? (
|
|
234
|
+
<div className="bg-[var(--bg-card)] border border-[var(--item-color-border)] overflow-hidden" style={{ borderRadius: 'var(--radius-card)' }}>
|
|
235
|
+
<table className="w-full">
|
|
236
|
+
<thead>
|
|
237
|
+
<tr className="border-b border-[var(--item-color-border)]">
|
|
238
|
+
{columns.filter(col => visibleColumns.includes(col.key)).map((col) => (
|
|
239
|
+
<th
|
|
240
|
+
key={col.key}
|
|
241
|
+
className={`px-4 py-3 text-left text-sm font-medium text-[var(--text-secondary)] ${
|
|
242
|
+
col.sortable ? 'cursor-pointer hover:text-[var(--text-primary)]' : ''
|
|
243
|
+
}`}
|
|
244
|
+
onClick={() => col.sortable && handleSort(col.key)}
|
|
245
|
+
>
|
|
246
|
+
{col.label}
|
|
247
|
+
{sortColumn === col.key && (
|
|
248
|
+
<span className="ml-1">{sortDirection === 'asc' ? '↑' : '↓'}</span>
|
|
249
|
+
)}
|
|
250
|
+
</th>
|
|
251
|
+
))}
|
|
252
|
+
<th className="px-4 py-3 text-right text-sm font-medium text-[var(--text-secondary)]">
|
|
253
|
+
{t('common:actions.title')}
|
|
254
|
+
</th>
|
|
255
|
+
</tr>
|
|
256
|
+
</thead>
|
|
257
|
+
<tbody>
|
|
258
|
+
{data?.items.map((item) => (
|
|
259
|
+
<tr
|
|
260
|
+
key={item.id}
|
|
261
|
+
className="border-b border-[var(--item-color-border)] hover:bg-[var(--bg-hover)] cursor-pointer"
|
|
262
|
+
onClick={() => navigate(`${basePath}/${item.id}`)}
|
|
263
|
+
>
|
|
264
|
+
{visibleColumns.includes('name') && (
|
|
265
|
+
<td className="px-4 py-3 text-sm text-[var(--text-primary)] font-medium">
|
|
266
|
+
{item.name}
|
|
267
|
+
</td>
|
|
268
|
+
)}
|
|
269
|
+
{visibleColumns.includes('description') && (
|
|
270
|
+
<td className="px-4 py-3 text-sm text-[var(--text-secondary)]">
|
|
271
|
+
{item.description || '-'}
|
|
272
|
+
</td>
|
|
273
|
+
)}
|
|
274
|
+
{visibleColumns.includes('isActive') && (
|
|
275
|
+
<td className="px-4 py-3">
|
|
276
|
+
<span
|
|
277
|
+
className={`inline-flex items-center px-2 py-1 text-xs font-medium ${
|
|
278
|
+
item.isActive
|
|
279
|
+
? 'bg-green-100 text-green-800'
|
|
280
|
+
: 'bg-gray-100 text-gray-800'
|
|
281
|
+
}`}
|
|
282
|
+
style={{ borderRadius: 'var(--radius-badge)' }}
|
|
283
|
+
>
|
|
284
|
+
{item.isActive ? t('common:status.active') : t('common:status.inactive')}
|
|
285
|
+
</span>
|
|
286
|
+
</td>
|
|
287
|
+
)}
|
|
288
|
+
{visibleColumns.includes('createdAt') && (
|
|
289
|
+
<td className="px-4 py-3 text-sm text-[var(--text-secondary)]">
|
|
290
|
+
{new Date(item.createdAt).toLocaleDateString()}
|
|
291
|
+
</td>
|
|
292
|
+
)}
|
|
293
|
+
<td className="px-4 py-3 text-right">
|
|
294
|
+
<div className="flex items-center justify-end gap-2">
|
|
295
|
+
<button
|
|
296
|
+
onClick={(e) => {
|
|
297
|
+
e.stopPropagation();
|
|
298
|
+
navigate(`${basePath}/${item.id}/edit`);
|
|
299
|
+
}}
|
|
300
|
+
className="p-1 hover:bg-[var(--bg-tertiary)]"
|
|
301
|
+
style={{ borderRadius: 'var(--radius-button)' }}
|
|
302
|
+
>
|
|
303
|
+
<Pencil className="w-4 h-4 text-[var(--text-secondary)]" />
|
|
304
|
+
</button>
|
|
305
|
+
<button
|
|
306
|
+
onClick={(e) => {
|
|
307
|
+
e.stopPropagation();
|
|
308
|
+
handleDelete(item.id);
|
|
309
|
+
}}
|
|
310
|
+
className="p-1 hover:bg-[var(--error-bg)]"
|
|
311
|
+
style={{ borderRadius: 'var(--radius-button)' }}
|
|
312
|
+
>
|
|
313
|
+
<Trash2 className="w-4 h-4 text-[var(--error-text)]" />
|
|
314
|
+
</button>
|
|
315
|
+
</div>
|
|
316
|
+
</td>
|
|
317
|
+
</tr>
|
|
318
|
+
))}
|
|
319
|
+
</tbody>
|
|
320
|
+
</table>
|
|
321
|
+
</div>
|
|
322
|
+
) : (
|
|
323
|
+
/* Grid view - ⚠️ OBLIGATOIRE: utiliser EntityCard */
|
|
324
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
325
|
+
{data?.items.map((item) => (
|
|
326
|
+
<EntityCard
|
|
327
|
+
key={item.id}
|
|
328
|
+
avatar={{ letter: item.name[0].toUpperCase(), color: 'var(--color-accent-500)' }}
|
|
329
|
+
title={item.name}
|
|
330
|
+
subtitle={item.code}
|
|
331
|
+
description={item.description}
|
|
332
|
+
badge={item.isActive ? {
|
|
333
|
+
icon: Check,
|
|
334
|
+
tooltip: t('common:status.active'),
|
|
335
|
+
color: 'var(--success-text)'
|
|
336
|
+
} : undefined}
|
|
337
|
+
stats={`${t('common:createdAt')}: ${new Date(item.createdAt).toLocaleDateString()}`}
|
|
338
|
+
onClick={() => navigate(`${basePath}/${item.id}`)}
|
|
339
|
+
actions={[
|
|
340
|
+
{ label: t('common:actions.view'), onClick: () => navigate(`${basePath}/${item.id}`), variant: 'primary' },
|
|
341
|
+
]}
|
|
342
|
+
/>
|
|
343
|
+
))}
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
{/* Pagination */}
|
|
348
|
+
{data && (
|
|
349
|
+
<Pagination
|
|
350
|
+
page={data.page}
|
|
351
|
+
pageSize={data.pageSize}
|
|
352
|
+
totalCount={data.totalCount}
|
|
353
|
+
onPageChange={setPage}
|
|
354
|
+
onPageSizeChange={setPageSize}
|
|
355
|
+
/>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## TEMPLATE: HOOK PRÉFÉRENCES
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
// hooks/use$MODULE_PASCALPreferences.ts
|
|
368
|
+
|
|
369
|
+
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
|
|
370
|
+
|
|
371
|
+
const DEFAULT_COLUMNS = ['name', 'description', 'isActive', 'createdAt'];
|
|
372
|
+
|
|
373
|
+
export function use$MODULE_PASCALPreferences() {
|
|
374
|
+
const { preferences, updatePreference } = useUserPreferences();
|
|
375
|
+
const modulePrefs = preferences.$module || {};
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
// Getters with defaults
|
|
379
|
+
pageSize: modulePrefs.pageSize ?? 10,
|
|
380
|
+
sortColumn: modulePrefs.sortColumn ?? 'createdAt',
|
|
381
|
+
sortDirection: modulePrefs.sortDirection ?? 'desc',
|
|
382
|
+
filters: modulePrefs.filters ?? {},
|
|
383
|
+
visibleColumns: modulePrefs.visibleColumns ?? DEFAULT_COLUMNS,
|
|
384
|
+
viewMode: modulePrefs.viewMode ?? 'list',
|
|
385
|
+
|
|
386
|
+
// Setters
|
|
387
|
+
setPageSize: (size: number) =>
|
|
388
|
+
updatePreference('$module.pageSize', size),
|
|
389
|
+
setSortColumn: (col: string) =>
|
|
390
|
+
updatePreference('$module.sortColumn', col),
|
|
391
|
+
setSortDirection: (dir: 'asc' | 'desc') =>
|
|
392
|
+
updatePreference('$module.sortDirection', dir),
|
|
393
|
+
setFilters: (filters: Record<string, unknown>) =>
|
|
394
|
+
updatePreference('$module.filters', filters),
|
|
395
|
+
setVisibleColumns: (cols: string[]) =>
|
|
396
|
+
updatePreference('$module.visibleColumns', cols),
|
|
397
|
+
setViewMode: (mode: 'list' | 'grid') =>
|
|
398
|
+
updatePreference('$module.viewMode', mode),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## TEMPLATE: SERVICE API
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
// services/api/$moduleApi.ts
|
|
409
|
+
|
|
410
|
+
import { api } from './apiClient';
|
|
411
|
+
|
|
412
|
+
export interface $ENTITY_PASCALDto {
|
|
413
|
+
id: string;
|
|
414
|
+
name: string;
|
|
415
|
+
description: string | null;
|
|
416
|
+
isActive: boolean;
|
|
417
|
+
createdAt: string;
|
|
418
|
+
updatedAt: string | null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export interface Create$ENTITY_PASCALRequest {
|
|
422
|
+
name: string;
|
|
423
|
+
description?: string;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export interface Update$ENTITY_PASCALRequest {
|
|
427
|
+
name: string;
|
|
428
|
+
description?: string;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export interface PagedResult<T> {
|
|
432
|
+
items: T[];
|
|
433
|
+
totalCount: number;
|
|
434
|
+
page: number;
|
|
435
|
+
pageSize: number;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export interface QueryParameters {
|
|
439
|
+
page?: number;
|
|
440
|
+
pageSize?: number;
|
|
441
|
+
searchTerm?: string;
|
|
442
|
+
sortColumn?: string;
|
|
443
|
+
sortDirection?: 'asc' | 'desc';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export const $moduleApi = {
|
|
447
|
+
getAll: async (params: QueryParameters = {}): Promise<PagedResult<$ENTITY_PASCALDto>> => {
|
|
448
|
+
const queryParams = new URLSearchParams();
|
|
449
|
+
if (params.page) queryParams.set('page', params.page.toString());
|
|
450
|
+
if (params.pageSize) queryParams.set('pageSize', params.pageSize.toString());
|
|
451
|
+
if (params.searchTerm) queryParams.set('searchTerm', params.searchTerm);
|
|
452
|
+
if (params.sortColumn) queryParams.set('sortColumn', params.sortColumn);
|
|
453
|
+
if (params.sortDirection) queryParams.set('sortDirection', params.sortDirection);
|
|
454
|
+
|
|
455
|
+
return api.get(`/api/$module?${queryParams}`);
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
getById: async (id: string): Promise<$ENTITY_PASCALDto> => {
|
|
459
|
+
return api.get(`/api/$module/${id}`);
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
create: async (data: Create$ENTITY_PASCALRequest): Promise<$ENTITY_PASCALDto> => {
|
|
463
|
+
return api.post('/api/$module', data);
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
update: async (id: string, data: Update$ENTITY_PASCALRequest): Promise<$ENTITY_PASCALDto> => {
|
|
467
|
+
return api.put(`/api/$module/${id}`, data);
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
delete: async (id: string): Promise<void> => {
|
|
471
|
+
return api.delete(`/api/$module/${id}`);
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## TEMPLATE: ROUTES (App.tsx)
|
|
479
|
+
|
|
480
|
+
### ⚠️ RÈGLE CRITIQUE: ROUTES IMBRIQUÉES OBLIGATOIRES
|
|
481
|
+
|
|
482
|
+
React Router v7 **exige** des routes imbriquées (nested routes) pour les applications multi-modules.
|
|
483
|
+
|
|
484
|
+
**❌ INTERDIT - Routes plates (causent des redirections vers Home)**
|
|
485
|
+
```tsx
|
|
486
|
+
<Route path="$APPLICATION" element={<Navigate to="/$CONTEXT/$APPLICATION/$DEFAULT_MODULE" replace />} />
|
|
487
|
+
<Route path="$APPLICATION/$MODULE" element={<$MODULE_PASCALPage />} />
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**✅ OBLIGATOIRE - Routes imbriquées avec index**
|
|
491
|
+
```tsx
|
|
492
|
+
// Ajouter dans App.tsx
|
|
493
|
+
|
|
494
|
+
import { $MODULE_PASCALPage } from '@/pages/$CONTEXT/$MODULE/$MODULE_PASCALPage';
|
|
495
|
+
import { $MODULE_PASCALDetailPage } from '@/pages/$CONTEXT/$MODULE/$MODULE_PASCALDetailPage';
|
|
496
|
+
import { Create$MODULE_PASCALPage } from '@/pages/$CONTEXT/$MODULE/Create$MODULE_PASCALPage';
|
|
497
|
+
|
|
498
|
+
// Dans les routes - STRUCTURE IMBRIQUÉE
|
|
499
|
+
<Route path="$APPLICATION">
|
|
500
|
+
<Route index element={<Navigate to="$DEFAULT_MODULE" replace />} />
|
|
501
|
+
<Route path="$MODULE" element={<$MODULE_PASCALPage />} />
|
|
502
|
+
<Route path="$MODULE/new" element={<Create$MODULE_PASCALPage />} />
|
|
503
|
+
<Route path="$MODULE/:id" element={<$MODULE_PASCALDetailPage />} />
|
|
504
|
+
<Route path="$MODULE/:id/edit" element={<Create$MODULE_PASCALPage />} />
|
|
505
|
+
</Route>
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Pourquoi cette structure ?
|
|
509
|
+
|
|
510
|
+
| Aspect | Routes plates | Routes imbriquées |
|
|
511
|
+
|--------|--------------|-------------------|
|
|
512
|
+
| Matching | Ambigu entre siblings | Hiérarchique clair |
|
|
513
|
+
| Navigate | Doit être absolu | Peut être relatif |
|
|
514
|
+
| Outlet | Non supporté | Supporté |
|
|
515
|
+
| Redirect | Peut échouer | Fonctionne toujours |
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## CHECKLIST FRONTEND
|
|
520
|
+
|
|
521
|
+
| Vérification | Status |
|
|
522
|
+
|--------------|--------|
|
|
523
|
+
| ☐ Page principale créée | |
|
|
524
|
+
| ☐ Composant ListView créé | |
|
|
525
|
+
| ☐ Hook préférences créé | |
|
|
526
|
+
| ☐ Service API créé | |
|
|
527
|
+
| ☐ Routes ajoutées dans App.tsx | |
|
|
528
|
+
| ☐ Utilise apiClient (pas d'appel direct) | |
|
|
529
|
+
| ☐ Pas d'import Infrastructure | |
|
|
530
|
+
| ☐ npm run build réussi | |
|
|
531
|
+
| ☐ npm run lint réussi | |
|