@hed-hog/operations 0.0.305 → 0.0.309

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 (138) 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 +52 -4
  22. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  23. package/dist/controllers/operations-timesheets.controller.js +28 -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/dto/update-collaborator-type.dto.d.ts +3 -1
  66. package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
  67. package/dist/dto/update-collaborator-type.dto.js +2 -1
  68. package/dist/dto/update-collaborator-type.dto.js.map +1 -1
  69. package/dist/operations.service.d.ts +362 -271
  70. package/dist/operations.service.d.ts.map +1 -1
  71. package/dist/operations.service.js +1195 -1098
  72. package/dist/operations.service.js.map +1 -1
  73. package/dist/operations.service.spec.js +73 -22
  74. package/dist/operations.service.spec.js.map +1 -1
  75. package/hedhog/data/menu.yaml +19 -55
  76. package/hedhog/data/operations_collaborator_type.yaml +76 -76
  77. package/hedhog/data/route.yaml +52 -70
  78. package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
  79. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +8 -1
  80. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +15 -10
  81. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +108 -213
  82. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +251 -2039
  83. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +167 -60
  84. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +70 -301
  85. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +102 -51
  86. package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
  87. package/hedhog/frontend/app/_lib/types.ts.ejs +19 -24
  88. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +14 -9
  89. package/hedhog/frontend/app/approvals/page.tsx.ejs +843 -151
  90. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +457 -154
  91. package/hedhog/frontend/app/collaborators/page.tsx.ejs +118 -49
  92. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  93. package/hedhog/frontend/app/contracts/page.tsx.ejs +215 -617
  94. package/hedhog/frontend/app/departments/page.tsx.ejs +257 -113
  95. package/hedhog/frontend/app/projects/page.tsx.ejs +90 -51
  96. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +546 -118
  97. package/hedhog/frontend/app/time-off/page.tsx.ejs +400 -123
  98. package/hedhog/frontend/app/timesheets/page.tsx.ejs +647 -342
  99. package/hedhog/frontend/messages/en.json +148 -14
  100. package/hedhog/frontend/messages/pt.json +199 -56
  101. package/hedhog/table/operations_collaborator.yaml +18 -18
  102. package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
  103. package/hedhog/table/operations_collaborator_type.yaml +33 -33
  104. package/hedhog/table/operations_contract.yaml +0 -9
  105. package/hedhog/table/operations_contract_document.yaml +33 -33
  106. package/package.json +4 -4
  107. package/src/controllers/operations-approvals.controller.ts +9 -3
  108. package/src/controllers/operations-collaborators.controller.ts +15 -2
  109. package/src/controllers/operations-contracts.controller.ts +8 -92
  110. package/src/controllers/operations-org-structure.controller.ts +17 -4
  111. package/src/controllers/operations-projects.controller.ts +10 -4
  112. package/src/controllers/operations-timesheets.controller.ts +30 -8
  113. package/src/dto/create-collaborator-type.dto.ts +43 -43
  114. package/src/dto/create-collaborator.dto.ts +223 -223
  115. package/src/dto/list-approvals.dto.ts +12 -0
  116. package/src/dto/list-collaborator-types.dto.ts +20 -15
  117. package/src/dto/list-collaborators.dto.ts +34 -30
  118. package/src/dto/list-contracts.dto.ts +20 -0
  119. package/src/dto/list-departments.dto.ts +8 -0
  120. package/src/dto/list-projects.dto.ts +8 -0
  121. package/src/dto/list-schedule-adjustments.dto.ts +8 -0
  122. package/src/dto/list-time-off-requests.dto.ts +8 -0
  123. package/src/dto/list-timesheets.dto.ts +8 -0
  124. package/src/dto/reorder-collaborator-types.dto.ts +10 -0
  125. package/src/dto/update-collaborator-type.dto.ts +4 -3
  126. package/src/dto/update-collaborator.dto.ts +3 -3
  127. package/src/operations.service.spec.ts +96 -30
  128. package/src/operations.service.ts +1738 -1777
  129. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +0 -631
  130. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +0 -526
  131. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +0 -247
  132. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +0 -3520
  133. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +0 -380
  134. package/hedhog/frontend/app/team/page.tsx.ejs +0 -352
  135. package/hedhog/table/operations_contract_financial_term.yaml +0 -40
  136. package/hedhog/table/operations_contract_revision.yaml +0 -38
  137. package/hedhog/table/operations_contract_signature.yaml +0 -38
  138. 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,65 +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();
56
- const [search, setSearch] = useState('');
57
- const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>(
58
- 'all'
190
+ const sensors = useSensors(
191
+ useSensor(PointerSensor, { activationConstraint: { distance: 6 } })
59
192
  );
193
+ const [search, setSearch] = useState('');
194
+ const [statusFilter, setStatusFilter] = useState<
195
+ 'all' | 'active' | 'inactive'
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);
62
- const [editingType, setEditingType] = useState<OperationsCollaboratorType | null>(
63
- null
64
- );
208
+ const [isReordering, setIsReordering] = useState(false);
209
+ const [editingType, setEditingType] =
210
+ useState<OperationsCollaboratorType | null>(null);
65
211
  const [form, setForm] = useState<CollaboratorTypeFormState>(EMPTY_FORM_STATE);
212
+ const [orderedCollaboratorTypes, setOrderedCollaboratorTypes] = useState<
213
+ OperationsCollaboratorType[]
214
+ >([]);
66
215
 
67
216
  const {
68
- data: collaboratorTypes = [],
217
+ data: collaboratorTypesResponse,
69
218
  isLoading,
70
219
  refetch,
71
- } = useQuery<OperationsCollaboratorType[]>({
72
- 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
+ ],
73
229
  enabled: access.isDirector,
74
- queryFn: () =>
75
- 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>>(
76
240
  request,
77
- '/operations/collaborator-types'
78
- ),
241
+ `/operations/collaborator-types?${params.toString()}`
242
+ );
243
+ },
244
+ placeholderData: (previous) => previous,
79
245
  });
80
246
 
81
- const filteredTypes = useMemo(
82
- () =>
83
- collaboratorTypes.filter((collaboratorType) => {
84
- const matchesSearch = !search.trim()
85
- ? true
86
- : [
87
- collaboratorType.name,
88
- collaboratorType.slug,
89
- collaboratorType.category,
90
- collaboratorType.description,
91
- ]
92
- .filter(Boolean)
93
- .some((value) =>
94
- String(value)
95
- .toLowerCase()
96
- .includes(search.trim().toLowerCase())
97
- );
98
-
99
- const matchesStatus =
100
- statusFilter === 'all' ? true : collaboratorType.status === statusFilter;
101
-
102
- return matchesSearch && matchesStatus;
103
- }),
104
- [collaboratorTypes, search, statusFilter]
105
- );
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]);
106
260
 
107
261
  const statsCards = useMemo(
108
262
  () => [
@@ -115,7 +269,8 @@ export default function OperationsCollaboratorTypesPage() {
115
269
  {
116
270
  key: 'active',
117
271
  title: t('cards.active'),
118
- value: collaboratorTypes.filter((item) => item.status === 'active').length,
272
+ value: collaboratorTypes.filter((item) => item.status === 'active')
273
+ .length,
119
274
  icon: Briefcase,
120
275
  },
121
276
  {
@@ -130,13 +285,25 @@ export default function OperationsCollaboratorTypesPage() {
130
285
  {
131
286
  key: 'inactive',
132
287
  title: t('cards.inactive'),
133
- value: collaboratorTypes.filter((item) => item.status === 'inactive').length,
288
+ value: collaboratorTypes.filter((item) => item.status === 'inactive')
289
+ .length,
134
290
  icon: Briefcase,
135
291
  },
136
292
  ],
137
293
  [collaboratorTypes, t]
138
294
  );
139
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
+
140
307
  const updateForm = <K extends keyof CollaboratorTypeFormState>(
141
308
  field: K,
142
309
  value: CollaboratorTypeFormState[K]
@@ -160,7 +327,6 @@ export default function OperationsCollaboratorTypesPage() {
160
327
  slug: collaboratorType.slug ?? '',
161
328
  description: collaboratorType.description ?? '',
162
329
  category: collaboratorType.category ?? '',
163
- sortOrder: String(collaboratorType.sortOrder ?? 0),
164
330
  status: collaboratorType.status ?? 'active',
165
331
  });
166
332
  setIsSheetOpen(true);
@@ -186,7 +352,6 @@ export default function OperationsCollaboratorTypesPage() {
186
352
  slug: form.slug.trim() || null,
187
353
  description: form.description.trim() || null,
188
354
  category: form.category.trim() || null,
189
- sortOrder: Number(form.sortOrder || '0'),
190
355
  status: form.status,
191
356
  isActive: form.status === 'active',
192
357
  }
@@ -222,6 +387,51 @@ export default function OperationsCollaboratorTypesPage() {
222
387
  }
223
388
  };
224
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
+
225
435
  if (!access.isLoading && !access.isDirector) {
226
436
  return (
227
437
  <Page>
@@ -273,103 +483,196 @@ export default function OperationsCollaboratorTypesPage() {
273
483
 
274
484
  <KpiCardsGrid items={statsCards} columns={4} />
275
485
 
276
- <div className="flex flex-col gap-4">
277
- <SearchBar
278
- searchQuery={search}
279
- onSearchChange={setSearch}
280
- onSearch={() => undefined}
281
- placeholder={t('searchPlaceholder')}
282
- controls={[
283
- {
284
- id: 'status',
285
- type: 'select',
286
- value: statusFilter,
287
- onChange: (value) =>
288
- setStatusFilter(
289
- (value as 'all' | 'active' | 'inactive') ?? 'all'
290
- ),
291
- placeholder: commonT('labels.status'),
292
- options: [
293
- { value: 'all', label: commonT('filters.allStatuses') },
294
- { value: 'active', label: formatEnumLabel('active') },
295
- { value: 'inactive', label: formatEnumLabel('inactive') },
296
- ],
297
- },
298
- ]}
299
- />
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')}
300
553
  </div>
301
554
 
302
555
  {isLoading ? (
303
556
  <div className="rounded-md border px-4 py-6 text-sm text-muted-foreground">
304
557
  {commonT('actions.refresh')}...
305
558
  </div>
306
- ) : filteredTypes.length > 0 ? (
307
- <div className="overflow-x-auto rounded-md border">
308
- <Table>
309
- <TableHeader>
310
- <TableRow>
311
- <TableHead>{t('columns.name')}</TableHead>
312
- <TableHead>{t('columns.slug')}</TableHead>
313
- <TableHead>{t('columns.category')}</TableHead>
314
- <TableHead>{t('columns.description')}</TableHead>
315
- <TableHead>{t('columns.sortOrder')}</TableHead>
316
- <TableHead>{t('columns.collaborators')}</TableHead>
317
- <TableHead>{commonT('labels.status')}</TableHead>
318
- <TableHead className="text-right">
319
- {commonT('labels.actions')}
320
- </TableHead>
321
- </TableRow>
322
- </TableHeader>
323
- <TableBody>
324
- {filteredTypes.map((collaboratorType) => (
325
- <TableRow key={collaboratorType.id}>
326
- <TableCell className="font-medium">
327
- {collaboratorType.name}
328
- </TableCell>
329
- <TableCell>{collaboratorType.slug}</TableCell>
330
- <TableCell>
331
- {collaboratorType.category || commonT('labels.notAvailable')}
332
- </TableCell>
333
- <TableCell className="max-w-md text-sm text-muted-foreground">
334
- {collaboratorType.description || commonT('labels.notAvailable')}
335
- </TableCell>
336
- <TableCell>{Number(collaboratorType.sortOrder ?? 0)}</TableCell>
337
- <TableCell>
338
- {Number(collaboratorType.collaboratorCount ?? 0)}
339
- </TableCell>
340
- <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>
341
580
  <StatusBadge
342
- label={formatEnumLabel(collaboratorType.status)}
581
+ label={getStatusLabel(collaboratorType.status)}
343
582
  className={getStatusBadgeClass(collaboratorType.status)}
344
583
  />
345
- </TableCell>
346
- <TableCell>
347
- <div className="flex justify-end gap-2">
348
- <Button
349
- variant="outline"
350
- size="icon"
351
- className="cursor-pointer"
352
- onClick={() => openEditSheet(collaboratorType)}
353
- >
354
- <Pencil className="size-4" />
355
- </Button>
356
- <Button
357
- variant="outline"
358
- size="sm"
359
- className="cursor-pointer"
360
- onClick={() => void toggleStatus(collaboratorType)}
361
- >
362
- {collaboratorType.status === 'inactive'
363
- ? commonT('actions.activate')
364
- : commonT('actions.deactivate')}
365
- </Button>
366
- </div>
367
- </TableCell>
368
- </TableRow>
369
- ))}
370
- </TableBody>
371
- </Table>
372
- </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
+ )
373
676
  ) : (
374
677
  <EmptyState
375
678
  icon={<Briefcase className="size-12" />}
@@ -380,6 +683,18 @@ export default function OperationsCollaboratorTypesPage() {
380
683
  />
381
684
  )}
382
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
+
383
698
  <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
384
699
  <SheetContent className="w-full overflow-y-auto sm:max-w-xl">
385
700
  <SheetHeader>
@@ -390,7 +705,7 @@ export default function OperationsCollaboratorTypesPage() {
390
705
  </SheetHeader>
391
706
 
392
707
  <form
393
- className="mt-6 space-y-4"
708
+ className="mt-6 space-y-4 px-6"
394
709
  onSubmit={(event) => {
395
710
  event.preventDefault();
396
711
  void saveCollaboratorType();
@@ -442,26 +757,14 @@ export default function OperationsCollaboratorTypesPage() {
442
757
  <textarea
443
758
  id="type-description"
444
759
  value={form.description}
445
- onChange={(event) => updateForm('description', event.target.value)}
760
+ onChange={(event) =>
761
+ updateForm('description', event.target.value)
762
+ }
446
763
  className="min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
447
764
  placeholder={t('form.description')}
448
765
  />
449
766
  </div>
450
767
 
451
- <div className="space-y-2">
452
- <label className="text-sm font-medium" htmlFor="type-sort-order">
453
- {t('form.sortOrder')}
454
- </label>
455
- <input
456
- id="type-sort-order"
457
- type="number"
458
- value={form.sortOrder}
459
- onChange={(event) => updateForm('sortOrder', event.target.value)}
460
- className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
461
- placeholder="0"
462
- />
463
- </div>
464
-
465
768
  <div className="space-y-2">
466
769
  <label className="text-sm font-medium" htmlFor="type-status">
467
770
  {t('form.status')}
@@ -477,8 +780,8 @@ export default function OperationsCollaboratorTypesPage() {
477
780
  }
478
781
  className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
479
782
  >
480
- <option value="active">{formatEnumLabel('active')}</option>
481
- <option value="inactive">{formatEnumLabel('inactive')}</option>
783
+ <option value="active">{getStatusLabel('active')}</option>
784
+ <option value="inactive">{getStatusLabel('inactive')}</option>
482
785
  </select>
483
786
  </div>
484
787