@hed-hog/operations 0.0.303 → 0.0.305

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 (178) hide show
  1. package/README.md +200 -43
  2. package/dist/controllers/operations-approvals.controller.d.ts +9 -0
  3. package/dist/controllers/operations-approvals.controller.d.ts.map +1 -0
  4. package/dist/controllers/operations-approvals.controller.js +64 -0
  5. package/dist/controllers/operations-approvals.controller.js.map +1 -0
  6. package/dist/controllers/operations-collaborators.controller.d.ts +223 -0
  7. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -0
  8. package/dist/controllers/operations-collaborators.controller.js +96 -0
  9. package/dist/controllers/operations-collaborators.controller.js.map +1 -0
  10. package/dist/controllers/operations-contracts.controller.d.ts +683 -0
  11. package/dist/controllers/operations-contracts.controller.d.ts.map +1 -0
  12. package/dist/controllers/operations-contracts.controller.js +198 -0
  13. package/dist/controllers/operations-contracts.controller.js.map +1 -0
  14. package/dist/controllers/operations-org-structure.controller.d.ts +108 -0
  15. package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -0
  16. package/dist/controllers/operations-org-structure.controller.js +143 -0
  17. package/dist/controllers/operations-org-structure.controller.js.map +1 -0
  18. package/dist/controllers/operations-projects.controller.d.ts +184 -0
  19. package/dist/controllers/operations-projects.controller.d.ts.map +1 -0
  20. package/dist/controllers/operations-projects.controller.js +87 -0
  21. package/dist/controllers/operations-projects.controller.js.map +1 -0
  22. package/dist/controllers/operations-tasks.controller.d.ts +85 -0
  23. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -0
  24. package/dist/controllers/operations-tasks.controller.js +90 -0
  25. package/dist/controllers/operations-tasks.controller.js.map +1 -0
  26. package/dist/controllers/operations-timesheets.controller.d.ts +99 -0
  27. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -0
  28. package/dist/controllers/operations-timesheets.controller.js +154 -0
  29. package/dist/controllers/operations-timesheets.controller.js.map +1 -0
  30. package/dist/dto/create-collaborator-type.dto.d.ts +10 -0
  31. package/dist/dto/create-collaborator-type.dto.d.ts.map +1 -0
  32. package/dist/dto/create-collaborator-type.dto.js +56 -0
  33. package/dist/dto/create-collaborator-type.dto.js.map +1 -0
  34. package/dist/dto/create-collaborator.dto.d.ts +42 -0
  35. package/dist/dto/create-collaborator.dto.d.ts.map +1 -0
  36. package/dist/dto/create-collaborator.dto.js +228 -0
  37. package/dist/dto/create-collaborator.dto.js.map +1 -0
  38. package/dist/dto/create-schedule-adjustment-request.dto.d.ts +17 -0
  39. package/dist/dto/create-schedule-adjustment-request.dto.d.ts.map +1 -0
  40. package/dist/dto/create-schedule-adjustment-request.dto.js +89 -0
  41. package/dist/dto/create-schedule-adjustment-request.dto.js.map +1 -0
  42. package/dist/dto/create-task.dto.d.ts +14 -0
  43. package/dist/dto/create-task.dto.d.ts.map +1 -0
  44. package/dist/dto/create-task.dto.js +83 -0
  45. package/dist/dto/create-task.dto.js.map +1 -0
  46. package/dist/dto/create-time-off-request.dto.d.ts +9 -0
  47. package/dist/dto/create-time-off-request.dto.d.ts.map +1 -0
  48. package/dist/dto/create-time-off-request.dto.js +54 -0
  49. package/dist/dto/create-time-off-request.dto.js.map +1 -0
  50. package/dist/dto/create-timesheet-entry.dto.d.ts +12 -0
  51. package/dist/dto/create-timesheet-entry.dto.d.ts.map +1 -0
  52. package/dist/dto/create-timesheet-entry.dto.js +75 -0
  53. package/dist/dto/create-timesheet-entry.dto.js.map +1 -0
  54. package/dist/dto/list-collaborator-types.dto.d.ts +4 -0
  55. package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -0
  56. package/dist/dto/list-collaborator-types.dto.js +29 -0
  57. package/dist/dto/list-collaborator-types.dto.js.map +1 -0
  58. package/dist/dto/list-collaborators.dto.d.ts +8 -0
  59. package/dist/dto/list-collaborators.dto.d.ts.map +1 -0
  60. package/dist/dto/list-collaborators.dto.js +42 -0
  61. package/dist/dto/list-collaborators.dto.js.map +1 -0
  62. package/dist/dto/list-project-options.dto.d.ts +4 -0
  63. package/dist/dto/list-project-options.dto.d.ts.map +1 -0
  64. package/dist/dto/list-project-options.dto.js +8 -0
  65. package/dist/dto/list-project-options.dto.js.map +1 -0
  66. package/dist/dto/list-tasks.dto.d.ts +7 -0
  67. package/dist/dto/list-tasks.dto.d.ts.map +1 -0
  68. package/dist/dto/list-tasks.dto.js +38 -0
  69. package/dist/dto/list-tasks.dto.js.map +1 -0
  70. package/dist/dto/list-timesheet-entries.dto.d.ts +10 -0
  71. package/dist/dto/list-timesheet-entries.dto.d.ts.map +1 -0
  72. package/dist/dto/list-timesheet-entries.dto.js +54 -0
  73. package/dist/dto/list-timesheet-entries.dto.js.map +1 -0
  74. package/dist/dto/update-collaborator-type.dto.d.ts +4 -0
  75. package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -0
  76. package/dist/dto/update-collaborator-type.dto.js +8 -0
  77. package/dist/dto/update-collaborator-type.dto.js.map +1 -0
  78. package/dist/dto/update-collaborator.dto.d.ts +4 -0
  79. package/dist/dto/update-collaborator.dto.d.ts.map +1 -0
  80. package/dist/dto/update-collaborator.dto.js +8 -0
  81. package/dist/dto/update-collaborator.dto.js.map +1 -0
  82. package/dist/dto/update-task.dto.d.ts +14 -0
  83. package/dist/dto/update-task.dto.d.ts.map +1 -0
  84. package/dist/dto/update-task.dto.js +84 -0
  85. package/dist/dto/update-task.dto.js.map +1 -0
  86. package/dist/operations.controller.d.ts +0 -1045
  87. package/dist/operations.controller.d.ts.map +1 -1
  88. package/dist/operations.controller.js +0 -429
  89. package/dist/operations.controller.js.map +1 -1
  90. package/dist/operations.module.d.ts.map +1 -1
  91. package/dist/operations.module.js +23 -2
  92. package/dist/operations.module.js.map +1 -1
  93. package/dist/operations.service.d.ts +429 -8
  94. package/dist/operations.service.d.ts.map +1 -1
  95. package/dist/operations.service.js +1931 -165
  96. package/dist/operations.service.js.map +1 -1
  97. package/dist/operations.service.spec.js +315 -1
  98. package/dist/operations.service.spec.js.map +1 -1
  99. package/dist/services/shared/operations-access.service.d.ts +16 -0
  100. package/dist/services/shared/operations-access.service.d.ts.map +1 -0
  101. package/dist/services/shared/operations-access.service.js +48 -0
  102. package/dist/services/shared/operations-access.service.js.map +1 -0
  103. package/hedhog/data/dashboard.yaml +20 -0
  104. package/hedhog/data/dashboard_component.yaml +274 -0
  105. package/hedhog/data/dashboard_component_role.yaml +174 -0
  106. package/hedhog/data/dashboard_item.yaml +299 -0
  107. package/hedhog/data/dashboard_role.yaml +20 -0
  108. package/hedhog/data/menu.yaml +30 -13
  109. package/hedhog/data/operations_collaborator_type.yaml +76 -0
  110. package/hedhog/data/route.yaml +196 -0
  111. package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +231 -0
  112. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +125 -40
  113. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +740 -106
  114. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -256
  115. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +7 -7
  116. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +306 -306
  117. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -247
  118. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -3520
  119. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +38 -16
  120. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +1504 -52
  121. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1017 -649
  122. package/hedhog/frontend/app/_components/section-card.tsx.ejs +25 -18
  123. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +609 -0
  124. package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +213 -0
  125. package/hedhog/frontend/app/_lib/api.ts.ejs +30 -1
  126. package/hedhog/frontend/app/_lib/types.ts.ejs +147 -39
  127. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +40 -9
  128. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +48 -1
  129. package/hedhog/frontend/app/approvals/page.tsx.ejs +116 -98
  130. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +502 -0
  131. package/hedhog/frontend/app/collaborators/page.tsx.ejs +116 -72
  132. package/hedhog/frontend/app/contracts/page.tsx.ejs +938 -938
  133. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +11 -9
  134. package/hedhog/frontend/app/departments/page.tsx.ejs +1 -1
  135. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +1 -1
  136. package/hedhog/frontend/app/projects/page.tsx.ejs +364 -133
  137. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +244 -120
  138. package/hedhog/frontend/app/team/page.tsx.ejs +15 -2
  139. package/hedhog/frontend/app/time-off/page.tsx.ejs +158 -82
  140. package/hedhog/frontend/app/timesheets/page.tsx.ejs +814 -357
  141. package/hedhog/frontend/messages/en.json +268 -53
  142. package/hedhog/frontend/messages/pt.json +484 -271
  143. package/hedhog/table/operations_collaborator.yaml +26 -13
  144. package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -0
  145. package/hedhog/table/operations_collaborator_type.yaml +33 -0
  146. package/hedhog/table/operations_job_title.yaml +24 -0
  147. package/hedhog/table/operations_project.yaml +9 -0
  148. package/hedhog/table/operations_project_assignment.yaml +9 -0
  149. package/hedhog/table/operations_project_role.yaml +39 -0
  150. package/hedhog/table/operations_task.yaml +69 -0
  151. package/hedhog/table/operations_timesheet_entry.yaml +12 -0
  152. package/package.json +6 -6
  153. package/src/controllers/operations-approvals.controller.ts +24 -0
  154. package/src/controllers/operations-collaborators.controller.ts +60 -0
  155. package/src/controllers/operations-contracts.controller.ts +138 -0
  156. package/src/controllers/operations-org-structure.controller.ts +92 -0
  157. package/src/controllers/operations-projects.controller.ts +50 -0
  158. package/src/controllers/operations-tasks.controller.ts +63 -0
  159. package/src/controllers/operations-timesheets.controller.ts +100 -0
  160. package/src/dto/create-collaborator-type.dto.ts +43 -0
  161. package/src/dto/create-collaborator.dto.ts +223 -0
  162. package/src/dto/create-schedule-adjustment-request.dto.ts +91 -0
  163. package/src/dto/create-task.dto.ts +75 -0
  164. package/src/dto/create-time-off-request.dto.ts +53 -0
  165. package/src/dto/create-timesheet-entry.dto.ts +67 -0
  166. package/src/dto/list-collaborator-types.dto.ts +15 -0
  167. package/src/dto/list-collaborators.dto.ts +30 -0
  168. package/src/dto/list-project-options.dto.ts +3 -0
  169. package/src/dto/list-tasks.dto.ts +25 -0
  170. package/src/dto/list-timesheet-entries.dto.ts +40 -0
  171. package/src/dto/update-collaborator-type.dto.ts +3 -0
  172. package/src/dto/update-collaborator.dto.ts +3 -0
  173. package/src/dto/update-task.dto.ts +76 -0
  174. package/src/operations.controller.ts +1 -278
  175. package/src/operations.module.ts +23 -2
  176. package/src/operations.service.spec.ts +450 -0
  177. package/src/operations.service.ts +4507 -1561
  178. package/src/services/shared/operations-access.service.ts +52 -0
@@ -362,15 +362,17 @@ export default function OperationsContractTemplatesPage() {
362
362
  <SheetDescription>{t('sheet.description')}</SheetDescription>
363
363
  </SheetHeader>
364
364
 
365
- <ContractTemplateFormScreen
366
- templateId={editingTemplate?.id}
367
- onCancel={() => setIsSheetOpen(false)}
368
- onSaved={async () => {
369
- setIsSheetOpen(false);
370
- setEditingTemplate(null);
371
- await refetch();
372
- }}
373
- />
365
+ <div className="px-4">
366
+ <ContractTemplateFormScreen
367
+ templateId={editingTemplate?.id}
368
+ onCancel={() => setIsSheetOpen(false)}
369
+ onSaved={async () => {
370
+ setIsSheetOpen(false);
371
+ setEditingTemplate(null);
372
+ await refetch();
373
+ }}
374
+ />
375
+ </div>
374
376
  </SheetContent>
375
377
  </Sheet>
376
378
  </Page>
@@ -370,7 +370,7 @@ export default function OperationsDepartmentsPage() {
370
370
  </SheetHeader>
371
371
 
372
372
  <form
373
- className="mt-6 space-y-4"
373
+ className="mt-6 space-y-4 px-4"
374
374
  onSubmit={(event) => {
375
375
  event.preventDefault();
376
376
  void saveDepartment();
@@ -7,5 +7,5 @@ export default async function OperationsProjectEditPage({
7
7
  }) {
8
8
  const { id } = await params;
9
9
 
10
- redirect(`/operations/projects?edit=${id}`);
10
+ redirect(`/operations/projects/${id}?edit=1`);
11
11
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
4
  import { Button } from '@/components/ui/button';
5
+ import { Card, CardContent } from '@/components/ui/card';
5
6
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
6
7
  import {
7
8
  Sheet,
@@ -18,12 +19,15 @@ import {
18
19
  TableHeader,
19
20
  TableRow,
20
21
  } from '@/components/ui/table';
22
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
21
23
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
22
24
  import {
23
25
  CalendarDays,
24
26
  Eye,
25
27
  FileText,
26
28
  FolderKanban,
29
+ LayoutGrid,
30
+ List,
27
31
  Pencil,
28
32
  PlayCircle,
29
33
  ShieldAlert,
@@ -44,6 +48,10 @@ import {
44
48
  getStatusBadgeClass,
45
49
  } from '../_lib/utils/format';
46
50
 
51
+ const PROJECT_VIEW_STORAGE_KEY = 'operations-projects-view-mode';
52
+
53
+ type ProjectViewMode = 'table' | 'cards';
54
+
47
55
  function parseEditProjectId(value: string | null) {
48
56
  const parsed = Number(value);
49
57
  return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
@@ -52,13 +60,23 @@ function parseEditProjectId(value: string | null) {
52
60
  export default function OperationsProjectsPage() {
53
61
  const t = useTranslations('operations.ProjectsPage');
54
62
  const commonT = useTranslations('operations.Common');
55
- const { request, showToastHandler, currentLocaleCode } = useApp();
63
+ const { request, showToastHandler, currentLocaleCode, getSettingValue } =
64
+ useApp();
56
65
  const access = useOperationsAccess();
57
66
  const router = useRouter();
58
67
  const pathname = usePathname();
59
68
  const searchParams = useSearchParams();
60
69
  const [search, setSearch] = useState('');
61
70
  const [statusFilter, setStatusFilter] = useState('all');
71
+ const [viewMode, setViewMode] = useState<ProjectViewMode>(() => {
72
+ if (typeof window === 'undefined') {
73
+ return 'table';
74
+ }
75
+
76
+ const savedViewMode = window.localStorage.getItem(PROJECT_VIEW_STORAGE_KEY);
77
+
78
+ return savedViewMode === 'cards' ? 'cards' : 'table';
79
+ });
62
80
 
63
81
  const createParam = searchParams.get('create');
64
82
  const editProjectId = parseEditProjectId(searchParams.get('edit'));
@@ -137,31 +155,55 @@ export default function OperationsProjectsPage() {
137
155
  {
138
156
  key: 'total',
139
157
  title: t('cards.total'),
158
+ description: t('cards.totalDescription'),
140
159
  value: projects.length,
141
160
  icon: FolderKanban,
161
+ accentClassName: 'from-slate-500/20 via-slate-400/10 to-transparent',
162
+ iconContainerClassName: 'bg-slate-100 text-slate-700',
142
163
  },
143
164
  {
144
165
  key: 'active',
145
166
  title: t('cards.active'),
167
+ description: t('cards.activeDescription'),
146
168
  value: projects.filter((item) => item.status === 'active').length,
147
169
  icon: PlayCircle,
170
+ accentClassName: 'from-green-500/20 via-emerald-500/10 to-transparent',
171
+ iconContainerClassName: 'bg-green-50 text-green-600',
148
172
  },
149
173
  {
150
174
  key: 'atRisk',
151
175
  title: t('cards.atRisk'),
176
+ description: t('cards.atRiskDescription'),
152
177
  value: projects.filter((item) => item.status === 'at_risk').length,
153
178
  icon: ShieldAlert,
179
+ accentClassName: 'from-amber-500/20 via-orange-500/10 to-transparent',
180
+ iconContainerClassName: 'bg-amber-50 text-amber-600',
154
181
  },
155
182
  {
156
183
  key: 'upcomingDeliveries',
157
184
  title: t('cards.upcomingDeliveries'),
185
+ description: t('cards.upcomingDeliveriesDescription'),
158
186
  value: projects.filter((item) => Boolean(item.endDate)).length,
159
187
  icon: CalendarDays,
188
+ accentClassName: 'from-blue-500/20 via-cyan-500/10 to-transparent',
189
+ iconContainerClassName: 'bg-blue-50 text-blue-600',
160
190
  },
161
191
  ],
162
192
  [projects, t]
163
193
  );
164
194
 
195
+ const handleViewModeChange = (value: string) => {
196
+ if (value !== 'table' && value !== 'cards') {
197
+ return;
198
+ }
199
+
200
+ setViewMode(value);
201
+
202
+ if (typeof window !== 'undefined') {
203
+ window.localStorage.setItem(PROJECT_VIEW_STORAGE_KEY, value);
204
+ }
205
+ };
206
+
165
207
  const toggleArchived = async (project: OperationsProject) => {
166
208
  const nextStatus = project.status === 'archived' ? 'active' : 'archived';
167
209
 
@@ -198,67 +240,81 @@ export default function OperationsProjectsPage() {
198
240
 
199
241
  <KpiCardsGrid items={statsCards} columns={4} />
200
242
 
201
- <SearchBar
202
- searchQuery={search}
203
- onSearchChange={setSearch}
204
- onSearch={() => undefined}
205
- placeholder={t('searchPlaceholder')}
206
- controls={[
207
- {
208
- id: 'status',
209
- type: 'select',
210
- value: statusFilter,
211
- onChange: setStatusFilter,
212
- placeholder: commonT('labels.status'),
213
- options: [
214
- { value: 'all', label: commonT('filters.allStatuses') },
215
- { value: 'planning', label: formatEnumLabel('planning') },
216
- { value: 'active', label: formatEnumLabel('active') },
217
- { value: 'at_risk', label: formatEnumLabel('at_risk') },
218
- { value: 'paused', label: formatEnumLabel('paused') },
219
- { value: 'completed', label: formatEnumLabel('completed') },
220
- { value: 'archived', label: formatEnumLabel('archived') },
221
- ],
222
- },
223
- ]}
224
- />
243
+ <div className="flex min-w-0 flex-col gap-4 xl:flex-row xl:items-center">
244
+ <div className="flex-1">
245
+ <SearchBar
246
+ className="w-full"
247
+ searchQuery={search}
248
+ onSearchChange={setSearch}
249
+ onSearch={() => undefined}
250
+ placeholder={t('searchPlaceholder')}
251
+ controls={[
252
+ {
253
+ id: 'status',
254
+ type: 'select',
255
+ value: statusFilter,
256
+ onChange: setStatusFilter,
257
+ placeholder: commonT('labels.status'),
258
+ options: [
259
+ { value: 'all', label: commonT('filters.allStatuses') },
260
+ { value: 'planning', label: formatEnumLabel('planning') },
261
+ { value: 'active', label: formatEnumLabel('active') },
262
+ { value: 'at_risk', label: formatEnumLabel('at_risk') },
263
+ { value: 'paused', label: formatEnumLabel('paused') },
264
+ { value: 'completed', label: formatEnumLabel('completed') },
265
+ { value: 'archived', label: formatEnumLabel('archived') },
266
+ ],
267
+ },
268
+ ]}
269
+ />
270
+ </div>
271
+
272
+ <div className="flex items-center justify-between gap-3 xl:justify-end">
273
+ <span className="text-xs font-medium text-muted-foreground">
274
+ {t('viewMode')}
275
+ </span>
276
+ <ToggleGroup
277
+ type="single"
278
+ value={viewMode}
279
+ onValueChange={handleViewModeChange}
280
+ variant="outline"
281
+ size="sm"
282
+ aria-label={t('viewMode')}
283
+ >
284
+ <ToggleGroupItem
285
+ value="table"
286
+ className="gap-1.5 px-2.5"
287
+ aria-label={t('viewModeTable')}
288
+ >
289
+ <List className="h-4 w-4" />
290
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
291
+ </ToggleGroupItem>
292
+ <ToggleGroupItem
293
+ value="cards"
294
+ className="gap-1.5 px-2.5"
295
+ aria-label={t('viewModeCards')}
296
+ >
297
+ <LayoutGrid className="h-4 w-4" />
298
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
299
+ </ToggleGroupItem>
300
+ </ToggleGroup>
301
+ </div>
302
+ </div>
225
303
 
226
304
  {filteredRows.length > 0 ? (
227
- <div className="overflow-x-auto rounded-md border">
228
- <Table className="table-fixed">
229
- <TableHeader>
230
- <TableRow>
231
- <TableHead className="w-[30%]">
232
- {commonT('labels.project')}
233
- </TableHead>
234
- <TableHead>{commonT('labels.client')}</TableHead>
235
- <TableHead>{commonT('labels.status')}</TableHead>
236
- <TableHead className="hidden lg:table-cell">
237
- {commonT('labels.manager')}
238
- </TableHead>
239
- <TableHead className="hidden md:table-cell">
240
- {commonT('labels.teamSize')}
241
- </TableHead>
242
- <TableHead className="hidden xl:table-cell">
243
- {commonT('labels.startDate')}
244
- </TableHead>
245
- <TableHead className="hidden xl:table-cell">
246
- {commonT('labels.endDate')}
247
- </TableHead>
248
- <TableHead className="hidden 2xl:table-cell">
249
- {commonT('labels.contractStatus')}
250
- </TableHead>
251
- <TableHead className="w-30 text-right sm:w-42.5">
252
- {commonT('labels.actions')}
253
- </TableHead>
254
- </TableRow>
255
- </TableHeader>
256
- <TableBody>
257
- {filteredRows.map((project) => (
258
- <TableRow key={project.id} className="hover:bg-muted/30">
259
- <TableCell>
305
+ viewMode === 'cards' ? (
306
+ <div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
307
+ {filteredRows.map((project) => (
308
+ <Card
309
+ key={project.id}
310
+ className="cursor-pointer overflow-hidden border-border/60 py-0 shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md"
311
+ >
312
+ <CardContent className="space-y-4 p-4">
313
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
260
314
  <div className="min-w-0">
261
- <div className="truncate font-medium">{project.name}</div>
315
+ <div className="truncate font-semibold">
316
+ {project.name}
317
+ </div>
262
318
  <div className="truncate text-xs text-muted-foreground">
263
319
  {[
264
320
  project.code,
@@ -269,96 +325,271 @@ export default function OperationsProjectsPage() {
269
325
  .join(' • ') || commonT('labels.notAvailable')}
270
326
  </div>
271
327
  </div>
272
- </TableCell>
273
- <TableCell>
274
- <div className="truncate">
275
- {project.clientName || commonT('labels.notAvailable')}
276
- </div>
277
- </TableCell>
278
- <TableCell>
279
328
  <StatusBadge
280
329
  label={formatEnumLabel(project.status)}
281
330
  className={getStatusBadgeClass(project.status)}
282
331
  />
283
- </TableCell>
284
- <TableCell className="hidden lg:table-cell">
285
- <div className="truncate">
332
+ </div>
333
+
334
+ <div className="grid gap-2 text-sm text-muted-foreground lg:grid-cols-2">
335
+ <div>
336
+ <span className="font-medium text-foreground">
337
+ {commonT('labels.client')}:
338
+ </span>{' '}
339
+ {project.clientName || commonT('labels.notAvailable')}
340
+ </div>
341
+ <div>
342
+ <span className="font-medium text-foreground">
343
+ {commonT('labels.manager')}:
344
+ </span>{' '}
286
345
  {project.managerName || commonT('labels.notAssigned')}
287
346
  </div>
288
- </TableCell>
289
- <TableCell className="hidden md:table-cell">
290
- {project.teamSize ?? 0}
291
- </TableCell>
292
- <TableCell className="hidden xl:table-cell">
293
- {formatDate(project.startDate)}
294
- </TableCell>
295
- <TableCell className="hidden xl:table-cell">
296
- {formatDate(project.endDate)}
297
- </TableCell>
298
- <TableCell className="hidden 2xl:table-cell">
299
- {project.contractStatus ? (
300
- <StatusBadge
301
- label={formatEnumLabel(project.contractStatus)}
302
- className={getStatusBadgeClass(project.contractStatus)}
303
- />
304
- ) : (
305
- commonT('labels.notAssigned')
306
- )}
307
- </TableCell>
308
- <TableCell>
309
- <div className="flex flex-wrap justify-end gap-1.5 sm:gap-2">
310
- <Button variant="outline" size="icon" asChild>
311
- <Link href={`/operations/projects/${project.id}`}>
312
- <Eye className="size-4" />
313
- </Link>
347
+ <div>
348
+ <span className="font-medium text-foreground">
349
+ {commonT('labels.teamSize')}:
350
+ </span>{' '}
351
+ {project.teamSize ?? 0}
352
+ </div>
353
+ <div>
354
+ <span className="font-medium text-foreground">
355
+ {commonT('labels.startDate')}:
356
+ </span>{' '}
357
+ {formatDate(
358
+ project.startDate,
359
+ getSettingValue,
360
+ currentLocaleCode
361
+ )}
362
+ </div>
363
+ <div>
364
+ <span className="font-medium text-foreground">
365
+ {commonT('labels.endDate')}:
366
+ </span>{' '}
367
+ {formatDate(
368
+ project.endDate,
369
+ getSettingValue,
370
+ currentLocaleCode
371
+ )}
372
+ </div>
373
+ <div className="flex items-center gap-2">
374
+ <span className="font-medium text-foreground">
375
+ {commonT('labels.contractStatus')}:
376
+ </span>
377
+ {project.contractStatus ? (
378
+ <StatusBadge
379
+ label={formatEnumLabel(project.contractStatus)}
380
+ className={getStatusBadgeClass(
381
+ project.contractStatus
382
+ )}
383
+ />
384
+ ) : (
385
+ <span>{commonT('labels.notAssigned')}</span>
386
+ )}
387
+ </div>
388
+ </div>
389
+
390
+ <div className="flex flex-wrap justify-end gap-2 border-t border-border/60 pt-3">
391
+ <Button variant="outline" size="icon" asChild>
392
+ <Link href={`/operations/projects/${project.id}`}>
393
+ <Eye className="size-4" />
394
+ </Link>
395
+ </Button>
396
+ {access.isDirector ? (
397
+ <Button
398
+ variant="outline"
399
+ size="icon"
400
+ className="cursor-pointer"
401
+ onClick={() => openEditSheet(project.id)}
402
+ >
403
+ <Pencil className="size-4" />
314
404
  </Button>
315
- {access.isDirector ? (
316
- <Button
317
- variant="outline"
318
- size="icon"
319
- className="cursor-pointer"
320
- onClick={() => openEditSheet(project.id)}
405
+ ) : null}
406
+ <Button
407
+ variant="outline"
408
+ size="icon"
409
+ asChild={Boolean(project.contractId)}
410
+ disabled={!project.contractId}
411
+ >
412
+ {project.contractId ? (
413
+ <Link
414
+ href={`/operations/contracts?edit=${project.contractId}`}
321
415
  >
322
- <Pencil className="size-4" />
323
- </Button>
324
- ) : null}
416
+ <FileText className="size-4" />
417
+ </Link>
418
+ ) : (
419
+ <span>
420
+ <FileText className="size-4" />
421
+ </span>
422
+ )}
423
+ </Button>
424
+ {access.isDirector ? (
325
425
  <Button
326
426
  variant="outline"
327
- size="icon"
328
- asChild={Boolean(project.contractId)}
329
- disabled={!project.contractId}
427
+ size="sm"
428
+ className="cursor-pointer"
429
+ onClick={() => void toggleArchived(project)}
330
430
  >
331
- {project.contractId ? (
332
- <Link
333
- href={`/operations/contracts?edit=${project.contractId}`}
334
- >
335
- <FileText className="size-4" />
336
- </Link>
337
- ) : (
338
- <span>
339
- <FileText className="size-4" />
340
- </span>
341
- )}
431
+ {project.status === 'archived'
432
+ ? commonT('actions.activate')
433
+ : t('actions.archive')}
342
434
  </Button>
343
- {access.isDirector ? (
435
+ ) : null}
436
+ </div>
437
+ </CardContent>
438
+ </Card>
439
+ ))}
440
+ </div>
441
+ ) : (
442
+ <div className="overflow-x-auto rounded-md border">
443
+ <Table className="table-fixed">
444
+ <TableHeader>
445
+ <TableRow>
446
+ <TableHead className="w-[30%]">
447
+ {commonT('labels.project')}
448
+ </TableHead>
449
+ <TableHead>{commonT('labels.client')}</TableHead>
450
+ <TableHead>{commonT('labels.status')}</TableHead>
451
+ <TableHead className="hidden lg:table-cell">
452
+ {commonT('labels.manager')}
453
+ </TableHead>
454
+ <TableHead className="hidden md:table-cell">
455
+ {commonT('labels.teamSize')}
456
+ </TableHead>
457
+ <TableHead className="hidden xl:table-cell">
458
+ {commonT('labels.startDate')}
459
+ </TableHead>
460
+ <TableHead className="hidden xl:table-cell">
461
+ {commonT('labels.endDate')}
462
+ </TableHead>
463
+ <TableHead className="hidden 2xl:table-cell">
464
+ {commonT('labels.contractStatus')}
465
+ </TableHead>
466
+ <TableHead className="w-30 text-right sm:w-42.5">
467
+ {commonT('labels.actions')}
468
+ </TableHead>
469
+ </TableRow>
470
+ </TableHeader>
471
+ <TableBody>
472
+ {filteredRows.map((project) => (
473
+ <TableRow
474
+ key={project.id}
475
+ className="cursor-pointer hover:bg-muted/30"
476
+ >
477
+ <TableCell>
478
+ <div className="min-w-0">
479
+ <div className="truncate font-medium">
480
+ {project.name}
481
+ </div>
482
+ <div className="truncate text-xs text-muted-foreground">
483
+ {[
484
+ project.code,
485
+ project.myRoleLabel,
486
+ project.contractName,
487
+ ]
488
+ .filter(Boolean)
489
+ .join(' • ') || commonT('labels.notAvailable')}
490
+ </div>
491
+ </div>
492
+ </TableCell>
493
+ <TableCell>
494
+ <div className="truncate">
495
+ {project.clientName || commonT('labels.notAvailable')}
496
+ </div>
497
+ </TableCell>
498
+ <TableCell>
499
+ <StatusBadge
500
+ label={formatEnumLabel(project.status)}
501
+ className={getStatusBadgeClass(project.status)}
502
+ />
503
+ </TableCell>
504
+ <TableCell className="hidden lg:table-cell">
505
+ <div className="truncate">
506
+ {project.managerName || commonT('labels.notAssigned')}
507
+ </div>
508
+ </TableCell>
509
+ <TableCell className="hidden md:table-cell">
510
+ {project.teamSize ?? 0}
511
+ </TableCell>
512
+ <TableCell className="hidden xl:table-cell">
513
+ {formatDate(
514
+ project.startDate,
515
+ getSettingValue,
516
+ currentLocaleCode
517
+ )}
518
+ </TableCell>
519
+ <TableCell className="hidden xl:table-cell">
520
+ {formatDate(
521
+ project.endDate,
522
+ getSettingValue,
523
+ currentLocaleCode
524
+ )}
525
+ </TableCell>
526
+ <TableCell className="hidden 2xl:table-cell">
527
+ {project.contractStatus ? (
528
+ <StatusBadge
529
+ label={formatEnumLabel(project.contractStatus)}
530
+ className={getStatusBadgeClass(
531
+ project.contractStatus
532
+ )}
533
+ />
534
+ ) : (
535
+ commonT('labels.notAssigned')
536
+ )}
537
+ </TableCell>
538
+ <TableCell>
539
+ <div className="flex flex-wrap justify-end gap-1.5 sm:gap-2">
540
+ <Button variant="outline" size="icon" asChild>
541
+ <Link href={`/operations/projects/${project.id}`}>
542
+ <Eye className="size-4" />
543
+ </Link>
544
+ </Button>
545
+ {access.isDirector ? (
546
+ <Button
547
+ variant="outline"
548
+ size="icon"
549
+ className="cursor-pointer"
550
+ onClick={() => openEditSheet(project.id)}
551
+ >
552
+ <Pencil className="size-4" />
553
+ </Button>
554
+ ) : null}
344
555
  <Button
345
556
  variant="outline"
346
- size="sm"
347
- className="cursor-pointer"
348
- onClick={() => void toggleArchived(project)}
557
+ size="icon"
558
+ asChild={Boolean(project.contractId)}
559
+ disabled={!project.contractId}
349
560
  >
350
- {project.status === 'archived'
351
- ? commonT('actions.activate')
352
- : t('actions.archive')}
561
+ {project.contractId ? (
562
+ <Link
563
+ href={`/operations/contracts?edit=${project.contractId}`}
564
+ >
565
+ <FileText className="size-4" />
566
+ </Link>
567
+ ) : (
568
+ <span>
569
+ <FileText className="size-4" />
570
+ </span>
571
+ )}
353
572
  </Button>
354
- ) : null}
355
- </div>
356
- </TableCell>
357
- </TableRow>
358
- ))}
359
- </TableBody>
360
- </Table>
361
- </div>
573
+ {access.isDirector ? (
574
+ <Button
575
+ variant="outline"
576
+ size="sm"
577
+ className="cursor-pointer"
578
+ onClick={() => void toggleArchived(project)}
579
+ >
580
+ {project.status === 'archived'
581
+ ? commonT('actions.activate')
582
+ : t('actions.archive')}
583
+ </Button>
584
+ ) : null}
585
+ </div>
586
+ </TableCell>
587
+ </TableRow>
588
+ ))}
589
+ </TableBody>
590
+ </Table>
591
+ </div>
592
+ )
362
593
  ) : (
363
594
  <EmptyState
364
595
  icon={<FolderKanban className="size-12" />}