@hed-hog/operations 0.0.306 → 0.0.310

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 (123) hide show
  1. package/dist/controllers/operations-approvals.controller.d.ts +114 -1
  2. package/dist/controllers/operations-approvals.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-approvals.controller.js +16 -3
  4. package/dist/controllers/operations-approvals.controller.js.map +1 -1
  5. package/dist/controllers/operations-collaborators.controller.d.ts +16 -1
  6. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  7. package/dist/controllers/operations-collaborators.controller.js +16 -3
  8. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  9. package/dist/controllers/operations-contracts.controller.d.ts +14 -453
  10. package/dist/controllers/operations-contracts.controller.d.ts.map +1 -1
  11. package/dist/controllers/operations-contracts.controller.js +11 -112
  12. package/dist/controllers/operations-contracts.controller.js.map +1 -1
  13. package/dist/controllers/operations-org-structure.controller.d.ts +65 -2
  14. package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -1
  15. package/dist/controllers/operations-org-structure.controller.js +18 -5
  16. package/dist/controllers/operations-org-structure.controller.js.map +1 -1
  17. package/dist/controllers/operations-projects.controller.d.ts +28 -4
  18. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  19. package/dist/controllers/operations-projects.controller.js +17 -5
  20. package/dist/controllers/operations-projects.controller.js.map +1 -1
  21. package/dist/controllers/operations-timesheets.controller.d.ts +31 -4
  22. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  23. package/dist/controllers/operations-timesheets.controller.js +16 -11
  24. package/dist/controllers/operations-timesheets.controller.js.map +1 -1
  25. package/dist/dto/list-approvals.dto.d.ts +6 -0
  26. package/dist/dto/list-approvals.dto.d.ts.map +1 -0
  27. package/dist/dto/list-approvals.dto.js +28 -0
  28. package/dist/dto/list-approvals.dto.js.map +1 -0
  29. package/dist/dto/list-collaborator-types.dto.d.ts +3 -1
  30. package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -1
  31. package/dist/dto/list-collaborator-types.dto.js +7 -1
  32. package/dist/dto/list-collaborator-types.dto.js.map +1 -1
  33. package/dist/dto/list-collaborators.dto.d.ts +1 -0
  34. package/dist/dto/list-collaborators.dto.d.ts.map +1 -1
  35. package/dist/dto/list-collaborators.dto.js +5 -0
  36. package/dist/dto/list-collaborators.dto.js.map +1 -1
  37. package/dist/dto/list-contracts.dto.d.ts +8 -0
  38. package/dist/dto/list-contracts.dto.d.ts.map +1 -0
  39. package/dist/dto/list-contracts.dto.js +38 -0
  40. package/dist/dto/list-contracts.dto.js.map +1 -0
  41. package/dist/dto/list-departments.dto.d.ts +5 -0
  42. package/dist/dto/list-departments.dto.d.ts.map +1 -0
  43. package/dist/dto/list-departments.dto.js +23 -0
  44. package/dist/dto/list-departments.dto.js.map +1 -0
  45. package/dist/dto/list-projects.dto.d.ts +5 -0
  46. package/dist/dto/list-projects.dto.d.ts.map +1 -0
  47. package/dist/dto/list-projects.dto.js +23 -0
  48. package/dist/dto/list-projects.dto.js.map +1 -0
  49. package/dist/dto/list-schedule-adjustments.dto.d.ts +5 -0
  50. package/dist/dto/list-schedule-adjustments.dto.d.ts.map +1 -0
  51. package/dist/dto/list-schedule-adjustments.dto.js +23 -0
  52. package/dist/dto/list-schedule-adjustments.dto.js.map +1 -0
  53. package/dist/dto/list-time-off-requests.dto.d.ts +5 -0
  54. package/dist/dto/list-time-off-requests.dto.d.ts.map +1 -0
  55. package/dist/dto/list-time-off-requests.dto.js +23 -0
  56. package/dist/dto/list-time-off-requests.dto.js.map +1 -0
  57. package/dist/dto/list-timesheets.dto.d.ts +5 -0
  58. package/dist/dto/list-timesheets.dto.d.ts.map +1 -0
  59. package/dist/dto/list-timesheets.dto.js +23 -0
  60. package/dist/dto/list-timesheets.dto.js.map +1 -0
  61. package/dist/dto/reorder-collaborator-types.dto.d.ts +4 -0
  62. package/dist/dto/reorder-collaborator-types.dto.d.ts.map +1 -0
  63. package/dist/dto/reorder-collaborator-types.dto.js +25 -0
  64. package/dist/dto/reorder-collaborator-types.dto.js.map +1 -0
  65. package/dist/operations.service.d.ts +340 -271
  66. package/dist/operations.service.d.ts.map +1 -1
  67. package/dist/operations.service.js +1007 -1043
  68. package/dist/operations.service.js.map +1 -1
  69. package/dist/operations.service.spec.js +0 -22
  70. package/dist/operations.service.spec.js.map +1 -1
  71. package/hedhog/data/menu.yaml +0 -36
  72. package/hedhog/data/route.yaml +42 -73
  73. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +8 -1
  74. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +15 -10
  75. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +108 -213
  76. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +251 -2039
  77. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +167 -60
  78. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +70 -301
  79. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +102 -51
  80. package/hedhog/frontend/app/_lib/types.ts.ejs +19 -24
  81. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +14 -9
  82. package/hedhog/frontend/app/approvals/page.tsx.ejs +842 -150
  83. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +445 -153
  84. package/hedhog/frontend/app/collaborators/page.tsx.ejs +118 -49
  85. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  86. package/hedhog/frontend/app/contracts/page.tsx.ejs +215 -617
  87. package/hedhog/frontend/app/departments/page.tsx.ejs +257 -113
  88. package/hedhog/frontend/app/projects/page.tsx.ejs +90 -51
  89. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +412 -147
  90. package/hedhog/frontend/app/time-off/page.tsx.ejs +400 -123
  91. package/hedhog/frontend/app/timesheets/page.tsx.ejs +460 -365
  92. package/hedhog/frontend/messages/en.json +143 -14
  93. package/hedhog/frontend/messages/pt.json +192 -54
  94. package/hedhog/table/operations_contract.yaml +0 -9
  95. package/package.json +4 -4
  96. package/src/controllers/operations-approvals.controller.ts +9 -3
  97. package/src/controllers/operations-collaborators.controller.ts +15 -2
  98. package/src/controllers/operations-contracts.controller.ts +8 -92
  99. package/src/controllers/operations-org-structure.controller.ts +17 -4
  100. package/src/controllers/operations-projects.controller.ts +10 -4
  101. package/src/controllers/operations-timesheets.controller.ts +17 -8
  102. package/src/dto/list-approvals.dto.ts +12 -0
  103. package/src/dto/list-collaborator-types.dto.ts +7 -2
  104. package/src/dto/list-collaborators.dto.ts +4 -0
  105. package/src/dto/list-contracts.dto.ts +20 -0
  106. package/src/dto/list-departments.dto.ts +8 -0
  107. package/src/dto/list-projects.dto.ts +8 -0
  108. package/src/dto/list-schedule-adjustments.dto.ts +8 -0
  109. package/src/dto/list-time-off-requests.dto.ts +8 -0
  110. package/src/dto/list-timesheets.dto.ts +8 -0
  111. package/src/dto/reorder-collaborator-types.dto.ts +10 -0
  112. package/src/operations.service.spec.ts +0 -30
  113. package/src/operations.service.ts +1557 -1806
  114. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +0 -631
  115. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +0 -526
  116. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +0 -247
  117. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +0 -3520
  118. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +0 -380
  119. package/hedhog/frontend/app/team/page.tsx.ejs +0 -352
  120. package/hedhog/table/operations_contract_financial_term.yaml +0 -40
  121. package/hedhog/table/operations_contract_revision.yaml +0 -38
  122. package/hedhog/table/operations_contract_signature.yaml +0 -38
  123. package/hedhog/table/operations_contract_template.yaml +0 -58
@@ -1,7 +1,13 @@
1
1
  'use client';
2
2
 
3
- import { EmptyState, Page, SearchBar } from '@/components/entity-list';
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PaginationFooter,
7
+ SearchBar,
8
+ } from '@/components/entity-list';
4
9
  import { Button } from '@/components/ui/button';
10
+ import { Card, CardContent } from '@/components/ui/card';
5
11
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
6
12
  import {
7
13
  Sheet,
@@ -18,16 +24,42 @@ import {
18
24
  TableHeader,
19
25
  TableRow,
20
26
  } from '@/components/ui/table';
27
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
28
+ import {
29
+ closestCenter,
30
+ DndContext,
31
+ type DragEndEvent,
32
+ PointerSensor,
33
+ useSensor,
34
+ useSensors,
35
+ } from '@dnd-kit/core';
36
+ import {
37
+ arrayMove,
38
+ SortableContext,
39
+ useSortable,
40
+ verticalListSortingStrategy,
41
+ } from '@dnd-kit/sortable';
42
+ import { CSS } from '@dnd-kit/utilities';
21
43
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
22
- import { Briefcase, Pencil, Users } from 'lucide-react';
44
+ import {
45
+ Briefcase,
46
+ GripVertical,
47
+ LayoutGrid,
48
+ List,
49
+ Pencil,
50
+ Users,
51
+ } from 'lucide-react';
23
52
  import { useTranslations } from 'next-intl';
24
53
  import Link from 'next/link';
25
- import { useMemo, useState } from 'react';
54
+ import { useEffect, useMemo, useState } from 'react';
26
55
  import { OperationsHeader } from '../_components/operations-header';
27
56
  import { StatusBadge } from '../_components/status-badge';
28
57
  import { fetchOperations, mutateOperations } from '../_lib/api';
29
58
  import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
30
- import type { OperationsCollaboratorType } from '../_lib/types';
59
+ import type {
60
+ OperationsCollaboratorType,
61
+ PaginatedResponse,
62
+ } from '../_lib/types';
31
63
  import { formatEnumLabel, getStatusBadgeClass } from '../_lib/utils/format';
32
64
 
33
65
  type CollaboratorTypeFormState = {
@@ -35,7 +67,6 @@ type CollaboratorTypeFormState = {
35
67
  slug: string;
36
68
  description: string;
37
69
  category: string;
38
- sortOrder: string;
39
70
  status: 'active' | 'inactive';
40
71
  };
41
72
 
@@ -44,66 +75,188 @@ const EMPTY_FORM_STATE: CollaboratorTypeFormState = {
44
75
  slug: '',
45
76
  description: '',
46
77
  category: '',
47
- sortOrder: '0',
48
78
  status: 'active',
49
79
  };
80
+ const EMPTY_COLLABORATOR_TYPES: OperationsCollaboratorType[] = [];
81
+
82
+ function SortableCollaboratorTypeRow({
83
+ collaboratorType,
84
+ position,
85
+ commonT,
86
+ t,
87
+ onEdit,
88
+ onToggleStatus,
89
+ reorderEnabled,
90
+ getStatusLabel,
91
+ }: {
92
+ collaboratorType: OperationsCollaboratorType;
93
+ position: number;
94
+ commonT: ReturnType<typeof useTranslations>;
95
+ t: ReturnType<typeof useTranslations>;
96
+ onEdit: (collaboratorType: OperationsCollaboratorType) => void;
97
+ onToggleStatus: (collaboratorType: OperationsCollaboratorType) => void;
98
+ reorderEnabled: boolean;
99
+ getStatusLabel: (value?: string | null) => string;
100
+ }) {
101
+ const {
102
+ attributes,
103
+ listeners,
104
+ setActivatorNodeRef,
105
+ setNodeRef,
106
+ transform,
107
+ transition,
108
+ isDragging,
109
+ } = useSortable({
110
+ id: collaboratorType.id,
111
+ disabled: !reorderEnabled,
112
+ });
113
+
114
+ return (
115
+ <TableRow
116
+ ref={setNodeRef}
117
+ style={{
118
+ transform: CSS.Transform.toString(transform),
119
+ transition,
120
+ }}
121
+ className={isDragging ? 'bg-muted/40' : undefined}
122
+ >
123
+ <TableCell className="w-14">
124
+ <button
125
+ type="button"
126
+ ref={setActivatorNodeRef}
127
+ {...listeners}
128
+ {...attributes}
129
+ disabled={!reorderEnabled}
130
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-input bg-background text-muted-foreground transition hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
131
+ aria-label={t('columns.drag')}
132
+ >
133
+ <GripVertical className="size-4" />
134
+ </button>
135
+ </TableCell>
136
+ <TableCell className="font-medium">{position}</TableCell>
137
+ <TableCell className="font-medium">{collaboratorType.name}</TableCell>
138
+ <TableCell>{collaboratorType.slug}</TableCell>
139
+ <TableCell>
140
+ {collaboratorType.category || commonT('labels.notAvailable')}
141
+ </TableCell>
142
+ <TableCell className="max-w-md text-sm text-muted-foreground">
143
+ {collaboratorType.description || commonT('labels.notAvailable')}
144
+ </TableCell>
145
+ <TableCell>{Number(collaboratorType.collaboratorCount ?? 0)}</TableCell>
146
+ <TableCell>
147
+ <StatusBadge
148
+ label={getStatusLabel(collaboratorType.status)}
149
+ className={getStatusBadgeClass(collaboratorType.status)}
150
+ />
151
+ </TableCell>
152
+ <TableCell>
153
+ <div className="flex justify-end gap-2">
154
+ <Button
155
+ variant="outline"
156
+ size="icon"
157
+ className="cursor-pointer"
158
+ onClick={() => onEdit(collaboratorType)}
159
+ >
160
+ <Pencil className="size-4" />
161
+ </Button>
162
+ <Button
163
+ variant="outline"
164
+ size="sm"
165
+ className="cursor-pointer"
166
+ onClick={() => onToggleStatus(collaboratorType)}
167
+ >
168
+ {collaboratorType.status === 'inactive'
169
+ ? commonT('actions.activate')
170
+ : commonT('actions.deactivate')}
171
+ </Button>
172
+ </div>
173
+ </TableCell>
174
+ </TableRow>
175
+ );
176
+ }
50
177
 
51
178
  export default function OperationsCollaboratorTypesPage() {
52
179
  const t = useTranslations('operations.CollaboratorTypesPage');
53
180
  const commonT = useTranslations('operations.Common');
181
+ const collaboratorFormT = useTranslations('operations.CollaboratorFormPage');
54
182
  const { request, showToastHandler, currentLocaleCode } = useApp();
183
+ const getStatusLabel = (value?: string | null) =>
184
+ value
185
+ ? collaboratorFormT.has(`options.statuses.${value}`)
186
+ ? collaboratorFormT(`options.statuses.${value}`)
187
+ : formatEnumLabel(value)
188
+ : '-';
55
189
  const access = useOperationsAccess();
190
+ const sensors = useSensors(
191
+ useSensor(PointerSensor, { activationConstraint: { distance: 6 } })
192
+ );
56
193
  const [search, setSearch] = useState('');
57
194
  const [statusFilter, setStatusFilter] = useState<
58
195
  'all' | 'active' | 'inactive'
59
196
  >('all');
197
+ const [page, setPage] = useState(1);
198
+ const [pageSize, setPageSize] = useState(100);
199
+ const [viewMode, setViewMode] = useState<'table' | 'cards'>(() => {
200
+ if (typeof window === 'undefined') return 'table';
201
+ const saved = window.localStorage.getItem(
202
+ 'operations-collaborator-types-view-mode'
203
+ );
204
+ return saved === 'cards' ? 'cards' : 'table';
205
+ });
60
206
  const [isSheetOpen, setIsSheetOpen] = useState(false);
61
207
  const [isSaving, setIsSaving] = useState(false);
208
+ const [isReordering, setIsReordering] = useState(false);
62
209
  const [editingType, setEditingType] =
63
210
  useState<OperationsCollaboratorType | null>(null);
64
211
  const [form, setForm] = useState<CollaboratorTypeFormState>(EMPTY_FORM_STATE);
212
+ const [orderedCollaboratorTypes, setOrderedCollaboratorTypes] = useState<
213
+ OperationsCollaboratorType[]
214
+ >([]);
65
215
 
66
216
  const {
67
- data: collaboratorTypes = [],
217
+ data: collaboratorTypesResponse,
68
218
  isLoading,
69
219
  refetch,
70
- } = useQuery<OperationsCollaboratorType[]>({
71
- queryKey: ['operations-collaborator-types-page', currentLocaleCode],
220
+ } = useQuery<PaginatedResponse<OperationsCollaboratorType>>({
221
+ queryKey: [
222
+ 'operations-collaborator-types-page',
223
+ currentLocaleCode,
224
+ search,
225
+ statusFilter,
226
+ page,
227
+ pageSize,
228
+ ],
72
229
  enabled: access.isDirector,
73
- queryFn: () =>
74
- fetchOperations<OperationsCollaboratorType[]>(
230
+ queryFn: () => {
231
+ const params = new URLSearchParams({
232
+ page: String(page),
233
+ pageSize: String(pageSize),
234
+ sortField: 'sortOrder',
235
+ sortOrder: 'asc',
236
+ });
237
+ if (search.trim()) params.set('search', search.trim());
238
+ if (statusFilter !== 'all') params.set('status', statusFilter);
239
+ return fetchOperations<PaginatedResponse<OperationsCollaboratorType>>(
75
240
  request,
76
- '/operations/collaborator-types'
77
- ),
241
+ `/operations/collaborator-types?${params.toString()}`
242
+ );
243
+ },
244
+ placeholderData: (previous) => previous,
78
245
  });
79
246
 
80
- const filteredTypes = useMemo(
81
- () =>
82
- collaboratorTypes.filter((collaboratorType) => {
83
- const matchesSearch = !search.trim()
84
- ? true
85
- : [
86
- collaboratorType.name,
87
- collaboratorType.slug,
88
- collaboratorType.category,
89
- collaboratorType.description,
90
- ]
91
- .filter(Boolean)
92
- .some((value) =>
93
- String(value)
94
- .toLowerCase()
95
- .includes(search.trim().toLowerCase())
96
- );
97
-
98
- const matchesStatus =
99
- statusFilter === 'all'
100
- ? true
101
- : collaboratorType.status === statusFilter;
102
-
103
- return matchesSearch && matchesStatus;
104
- }),
105
- [collaboratorTypes, search, statusFilter]
106
- );
247
+ const collaboratorTypes =
248
+ collaboratorTypesResponse?.data ?? EMPTY_COLLABORATOR_TYPES;
249
+ const totalItems = collaboratorTypesResponse?.total ?? 0;
250
+ const reorderEnabled =
251
+ viewMode === 'table' &&
252
+ !search.trim() &&
253
+ statusFilter === 'all' &&
254
+ page === 1 &&
255
+ totalItems <= pageSize;
256
+
257
+ useEffect(() => {
258
+ setOrderedCollaboratorTypes(collaboratorTypes);
259
+ }, [collaboratorTypes]);
107
260
 
108
261
  const statsCards = useMemo(
109
262
  () => [
@@ -140,6 +293,17 @@ export default function OperationsCollaboratorTypesPage() {
140
293
  [collaboratorTypes, t]
141
294
  );
142
295
 
296
+ const handleViewModeChange = (value: string) => {
297
+ if (value !== 'table' && value !== 'cards') return;
298
+ setViewMode(value);
299
+ if (typeof window !== 'undefined') {
300
+ window.localStorage.setItem(
301
+ 'operations-collaborator-types-view-mode',
302
+ value
303
+ );
304
+ }
305
+ };
306
+
143
307
  const updateForm = <K extends keyof CollaboratorTypeFormState>(
144
308
  field: K,
145
309
  value: CollaboratorTypeFormState[K]
@@ -163,7 +327,6 @@ export default function OperationsCollaboratorTypesPage() {
163
327
  slug: collaboratorType.slug ?? '',
164
328
  description: collaboratorType.description ?? '',
165
329
  category: collaboratorType.category ?? '',
166
- sortOrder: String(collaboratorType.sortOrder ?? 0),
167
330
  status: collaboratorType.status ?? 'active',
168
331
  });
169
332
  setIsSheetOpen(true);
@@ -189,7 +352,6 @@ export default function OperationsCollaboratorTypesPage() {
189
352
  slug: form.slug.trim() || null,
190
353
  description: form.description.trim() || null,
191
354
  category: form.category.trim() || null,
192
- sortOrder: Number(form.sortOrder || '0'),
193
355
  status: form.status,
194
356
  isActive: form.status === 'active',
195
357
  }
@@ -225,6 +387,51 @@ export default function OperationsCollaboratorTypesPage() {
225
387
  }
226
388
  };
227
389
 
390
+ const handleDragEnd = async (event: DragEndEvent) => {
391
+ if (!reorderEnabled || isReordering) return;
392
+
393
+ const { active, over } = event;
394
+
395
+ if (!over || active.id === over.id) {
396
+ return;
397
+ }
398
+
399
+ const oldIndex = orderedCollaboratorTypes.findIndex(
400
+ (item) => item.id === Number(active.id)
401
+ );
402
+ const newIndex = orderedCollaboratorTypes.findIndex(
403
+ (item) => item.id === Number(over.id)
404
+ );
405
+
406
+ if (oldIndex < 0 || newIndex < 0) {
407
+ return;
408
+ }
409
+
410
+ const nextOrder = arrayMove(orderedCollaboratorTypes, oldIndex, newIndex);
411
+ const previousOrder = orderedCollaboratorTypes;
412
+
413
+ setOrderedCollaboratorTypes(nextOrder);
414
+ setIsReordering(true);
415
+
416
+ try {
417
+ await mutateOperations(
418
+ request,
419
+ '/operations/collaborator-types/reorder',
420
+ 'PATCH',
421
+ {
422
+ ids: nextOrder.map((item) => item.id),
423
+ }
424
+ );
425
+ showToastHandler?.('success', t('messages.reorderSuccess'));
426
+ await refetch();
427
+ } catch {
428
+ setOrderedCollaboratorTypes(previousOrder);
429
+ showToastHandler?.('error', t('messages.reorderError'));
430
+ } finally {
431
+ setIsReordering(false);
432
+ }
433
+ };
434
+
228
435
  if (!access.isLoading && !access.isDirector) {
229
436
  return (
230
437
  <Page>
@@ -276,107 +483,196 @@ export default function OperationsCollaboratorTypesPage() {
276
483
 
277
484
  <KpiCardsGrid items={statsCards} columns={4} />
278
485
 
279
- <div className="flex flex-col gap-4">
280
- <SearchBar
281
- searchQuery={search}
282
- onSearchChange={setSearch}
283
- onSearch={() => undefined}
284
- placeholder={t('searchPlaceholder')}
285
- controls={[
286
- {
287
- id: 'status',
288
- type: 'select',
289
- value: statusFilter,
290
- onChange: (value) =>
291
- setStatusFilter(
292
- (value as 'all' | 'active' | 'inactive') ?? 'all'
293
- ),
294
- placeholder: commonT('labels.status'),
295
- options: [
296
- { value: 'all', label: commonT('filters.allStatuses') },
297
- { value: 'active', label: formatEnumLabel('active') },
298
- { value: 'inactive', label: formatEnumLabel('inactive') },
299
- ],
300
- },
301
- ]}
302
- />
486
+ <div className="flex min-w-0 flex-col gap-4 xl:flex-row xl:items-center">
487
+ <div className="flex-1">
488
+ <SearchBar
489
+ searchQuery={search}
490
+ onSearchChange={(value) => {
491
+ setSearch(value);
492
+ setPage(1);
493
+ }}
494
+ showSearchButton={false}
495
+ debounceMs={500}
496
+ placeholder={t('searchPlaceholder')}
497
+ controls={[
498
+ {
499
+ id: 'status',
500
+ type: 'select',
501
+ value: statusFilter,
502
+ onChange: (value) => {
503
+ setStatusFilter(
504
+ (value as 'all' | 'active' | 'inactive') ?? 'all'
505
+ );
506
+ setPage(1);
507
+ },
508
+ placeholder: commonT('labels.status'),
509
+ options: [
510
+ { value: 'all', label: commonT('filters.allStatuses') },
511
+ { value: 'active', label: getStatusLabel('active') },
512
+ { value: 'inactive', label: getStatusLabel('inactive') },
513
+ ],
514
+ },
515
+ ]}
516
+ />
517
+ </div>
518
+
519
+ <div className="flex items-center justify-between gap-3 xl:justify-end">
520
+ <span className="text-xs font-medium text-muted-foreground">
521
+ {t('viewMode')}
522
+ </span>
523
+ <ToggleGroup
524
+ type="single"
525
+ value={viewMode}
526
+ onValueChange={handleViewModeChange}
527
+ variant="outline"
528
+ size="sm"
529
+ aria-label={t('viewMode')}
530
+ >
531
+ <ToggleGroupItem
532
+ value="table"
533
+ className="gap-1.5 px-2.5"
534
+ aria-label={t('viewModeTable')}
535
+ >
536
+ <List className="h-4 w-4" />
537
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
538
+ </ToggleGroupItem>
539
+ <ToggleGroupItem
540
+ value="cards"
541
+ className="gap-1.5 px-2.5"
542
+ aria-label={t('viewModeCards')}
543
+ >
544
+ <LayoutGrid className="h-4 w-4" />
545
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
546
+ </ToggleGroupItem>
547
+ </ToggleGroup>
548
+ </div>
549
+ </div>
550
+
551
+ <div className="rounded-md border border-dashed bg-muted/20 px-4 py-3 text-sm text-muted-foreground">
552
+ {reorderEnabled ? t('reorder.description') : t('reorder.filtered')}
303
553
  </div>
304
554
 
305
555
  {isLoading ? (
306
556
  <div className="rounded-md border px-4 py-6 text-sm text-muted-foreground">
307
557
  {commonT('actions.refresh')}...
308
558
  </div>
309
- ) : filteredTypes.length > 0 ? (
310
- <div className="overflow-x-auto rounded-md border">
311
- <Table>
312
- <TableHeader>
313
- <TableRow>
314
- <TableHead>{t('columns.name')}</TableHead>
315
- <TableHead>{t('columns.slug')}</TableHead>
316
- <TableHead>{t('columns.category')}</TableHead>
317
- <TableHead>{t('columns.description')}</TableHead>
318
- <TableHead>{t('columns.sortOrder')}</TableHead>
319
- <TableHead>{t('columns.collaborators')}</TableHead>
320
- <TableHead>{commonT('labels.status')}</TableHead>
321
- <TableHead className="text-right">
322
- {commonT('labels.actions')}
323
- </TableHead>
324
- </TableRow>
325
- </TableHeader>
326
- <TableBody>
327
- {filteredTypes.map((collaboratorType) => (
328
- <TableRow key={collaboratorType.id}>
329
- <TableCell className="font-medium">
330
- {collaboratorType.name}
331
- </TableCell>
332
- <TableCell>{collaboratorType.slug}</TableCell>
333
- <TableCell>
334
- {collaboratorType.category ||
335
- commonT('labels.notAvailable')}
336
- </TableCell>
337
- <TableCell className="max-w-md text-sm text-muted-foreground">
338
- {collaboratorType.description ||
339
- commonT('labels.notAvailable')}
340
- </TableCell>
341
- <TableCell>
342
- {Number(collaboratorType.sortOrder ?? 0)}
343
- </TableCell>
344
- <TableCell>
345
- {Number(collaboratorType.collaboratorCount ?? 0)}
346
- </TableCell>
347
- <TableCell>
559
+ ) : collaboratorTypes.length > 0 ? (
560
+ viewMode === 'cards' ? (
561
+ <div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
562
+ {orderedCollaboratorTypes.map((collaboratorType) => (
563
+ <Card
564
+ key={collaboratorType.id}
565
+ className="cursor-pointer overflow-hidden border-border/60 py-0 shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md"
566
+ onClick={() => openEditSheet(collaboratorType)}
567
+ >
568
+ <CardContent className="space-y-3 p-4">
569
+ <div className="flex items-start justify-between gap-3">
570
+ <div className="min-w-0">
571
+ <div className="truncate font-semibold">
572
+ {collaboratorType.name}
573
+ </div>
574
+ {collaboratorType.slug ? (
575
+ <div className="truncate text-xs text-muted-foreground">
576
+ {collaboratorType.slug}
577
+ </div>
578
+ ) : null}
579
+ </div>
348
580
  <StatusBadge
349
- label={formatEnumLabel(collaboratorType.status)}
581
+ label={getStatusLabel(collaboratorType.status)}
350
582
  className={getStatusBadgeClass(collaboratorType.status)}
351
583
  />
352
- </TableCell>
353
- <TableCell>
354
- <div className="flex justify-end gap-2">
355
- <Button
356
- variant="outline"
357
- size="icon"
358
- className="cursor-pointer"
359
- onClick={() => openEditSheet(collaboratorType)}
360
- >
361
- <Pencil className="size-4" />
362
- </Button>
363
- <Button
364
- variant="outline"
365
- size="sm"
366
- className="cursor-pointer"
367
- onClick={() => void toggleStatus(collaboratorType)}
368
- >
369
- {collaboratorType.status === 'inactive'
370
- ? commonT('actions.activate')
371
- : commonT('actions.deactivate')}
372
- </Button>
373
- </div>
374
- </TableCell>
375
- </TableRow>
376
- ))}
377
- </TableBody>
378
- </Table>
379
- </div>
584
+ </div>
585
+ <div className="flex flex-wrap gap-2">
586
+ {collaboratorType.category ? (
587
+ <span className="inline-flex items-center rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary">
588
+ {collaboratorType.category}
589
+ </span>
590
+ ) : null}
591
+ <span className="inline-flex items-center rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
592
+ {Number(collaboratorType.collaboratorCount ?? 0)}{' '}
593
+ {commonT('labels.collaborators')}
594
+ </span>
595
+ </div>
596
+ {collaboratorType.description ? (
597
+ <p className="line-clamp-2 text-sm text-muted-foreground">
598
+ {collaboratorType.description}
599
+ </p>
600
+ ) : null}
601
+ <div className="flex justify-end gap-2 border-t border-border/60 pt-3">
602
+ <Button
603
+ variant="outline"
604
+ size="icon"
605
+ onClick={(e) => {
606
+ e.stopPropagation();
607
+ openEditSheet(collaboratorType);
608
+ }}
609
+ >
610
+ <Pencil className="size-4" />
611
+ </Button>
612
+ <Button
613
+ variant="outline"
614
+ size="sm"
615
+ onClick={(e) => {
616
+ e.stopPropagation();
617
+ void toggleStatus(collaboratorType);
618
+ }}
619
+ >
620
+ {collaboratorType.status === 'inactive'
621
+ ? commonT('actions.activate')
622
+ : commonT('actions.deactivate')}
623
+ </Button>
624
+ </div>
625
+ </CardContent>
626
+ </Card>
627
+ ))}
628
+ </div>
629
+ ) : (
630
+ <div className="overflow-x-auto rounded-md border">
631
+ <DndContext
632
+ sensors={sensors}
633
+ collisionDetection={closestCenter}
634
+ onDragEnd={(event) => void handleDragEnd(event)}
635
+ >
636
+ <Table>
637
+ <TableHeader>
638
+ <TableRow>
639
+ <TableHead className="w-14">{t('columns.drag')}</TableHead>
640
+ <TableHead>{t('columns.sortOrder')}</TableHead>
641
+ <TableHead>{t('columns.name')}</TableHead>
642
+ <TableHead>{t('columns.slug')}</TableHead>
643
+ <TableHead>{t('columns.category')}</TableHead>
644
+ <TableHead>{t('columns.description')}</TableHead>
645
+ <TableHead>{t('columns.collaborators')}</TableHead>
646
+ <TableHead>{commonT('labels.status')}</TableHead>
647
+ <TableHead className="text-right">
648
+ {commonT('labels.actions')}
649
+ </TableHead>
650
+ </TableRow>
651
+ </TableHeader>
652
+ <TableBody>
653
+ <SortableContext
654
+ items={orderedCollaboratorTypes.map((item) => item.id)}
655
+ strategy={verticalListSortingStrategy}
656
+ >
657
+ {orderedCollaboratorTypes.map((collaboratorType, index) => (
658
+ <SortableCollaboratorTypeRow
659
+ key={collaboratorType.id}
660
+ collaboratorType={collaboratorType}
661
+ position={index + 1}
662
+ commonT={commonT}
663
+ t={t}
664
+ onEdit={openEditSheet}
665
+ onToggleStatus={(item) => void toggleStatus(item)}
666
+ reorderEnabled={reorderEnabled && !isReordering}
667
+ getStatusLabel={getStatusLabel}
668
+ />
669
+ ))}
670
+ </SortableContext>
671
+ </TableBody>
672
+ </Table>
673
+ </DndContext>
674
+ </div>
675
+ )
380
676
  ) : (
381
677
  <EmptyState
382
678
  icon={<Briefcase className="size-12" />}
@@ -387,6 +683,18 @@ export default function OperationsCollaboratorTypesPage() {
387
683
  />
388
684
  )}
389
685
 
686
+ <PaginationFooter
687
+ currentPage={page}
688
+ pageSize={pageSize}
689
+ totalItems={totalItems}
690
+ pageSizeOptions={[25, 50, 100, 200]}
691
+ onPageChange={setPage}
692
+ onPageSizeChange={(size) => {
693
+ setPageSize(size);
694
+ setPage(1);
695
+ }}
696
+ />
697
+
390
698
  <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
391
699
  <SheetContent className="w-full overflow-y-auto sm:max-w-xl">
392
700
  <SheetHeader>
@@ -397,7 +705,7 @@ export default function OperationsCollaboratorTypesPage() {
397
705
  </SheetHeader>
398
706
 
399
707
  <form
400
- className="mt-6 px-6 space-y-4"
708
+ className="mt-6 space-y-4 px-6"
401
709
  onSubmit={(event) => {
402
710
  event.preventDefault();
403
711
  void saveCollaboratorType();
@@ -457,22 +765,6 @@ export default function OperationsCollaboratorTypesPage() {
457
765
  />
458
766
  </div>
459
767
 
460
- <div className="space-y-2">
461
- <label className="text-sm font-medium" htmlFor="type-sort-order">
462
- {t('form.sortOrder')}
463
- </label>
464
- <input
465
- id="type-sort-order"
466
- type="number"
467
- value={form.sortOrder}
468
- onChange={(event) =>
469
- updateForm('sortOrder', event.target.value)
470
- }
471
- className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
472
- placeholder="0"
473
- />
474
- </div>
475
-
476
768
  <div className="space-y-2">
477
769
  <label className="text-sm font-medium" htmlFor="type-status">
478
770
  {t('form.status')}
@@ -488,8 +780,8 @@ export default function OperationsCollaboratorTypesPage() {
488
780
  }
489
781
  className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
490
782
  >
491
- <option value="active">{formatEnumLabel('active')}</option>
492
- <option value="inactive">{formatEnumLabel('inactive')}</option>
783
+ <option value="active">{getStatusLabel('active')}</option>
784
+ <option value="inactive">{getStatusLabel('inactive')}</option>
493
785
  </select>
494
786
  </div>
495
787