@hed-hog/operations 0.0.295 → 0.0.296

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 (126) hide show
  1. package/dist/operations.controller.d.ts +415 -0
  2. package/dist/operations.controller.d.ts.map +1 -0
  3. package/dist/operations.controller.js +333 -0
  4. package/dist/operations.controller.js.map +1 -0
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +4 -3
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.service.d.ts +589 -153
  9. package/dist/operations.service.d.ts.map +1 -1
  10. package/dist/operations.service.js +2229 -100
  11. package/dist/operations.service.js.map +1 -1
  12. package/hedhog/data/menu.yaml +198 -251
  13. package/hedhog/data/role.yaml +23 -14
  14. package/hedhog/data/route.yaml +317 -143
  15. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +310 -0
  16. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +631 -0
  17. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +132 -0
  18. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +558 -0
  19. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +291 -0
  20. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +689 -0
  21. package/hedhog/frontend/app/_lib/api.ts.ejs +32 -0
  22. package/hedhog/frontend/app/_lib/hooks/use-operations-access.ts.ejs +44 -0
  23. package/hedhog/frontend/app/_lib/types.ts.ejs +360 -0
  24. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +129 -25
  25. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +14 -0
  26. package/hedhog/frontend/app/approvals/page.tsx.ejs +386 -147
  27. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +11 -0
  28. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +11 -0
  29. package/hedhog/frontend/app/collaborators/new/page.tsx.ejs +5 -0
  30. package/hedhog/frontend/app/collaborators/page.tsx.ejs +261 -0
  31. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +11 -108
  33. package/hedhog/frontend/app/contracts/new/page.tsx.ejs +17 -0
  34. package/hedhog/frontend/app/contracts/page.tsx.ejs +262 -181
  35. package/hedhog/frontend/app/page.tsx.ejs +319 -177
  36. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +11 -0
  37. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +11 -936
  38. package/hedhog/frontend/app/projects/new/page.tsx.ejs +5 -0
  39. package/hedhog/frontend/app/projects/page.tsx.ejs +236 -1074
  40. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +418 -0
  41. package/hedhog/frontend/app/team/page.tsx.ejs +339 -0
  42. package/hedhog/frontend/app/time-off/page.tsx.ejs +328 -0
  43. package/hedhog/frontend/app/timesheets/page.tsx.ejs +636 -126
  44. package/hedhog/frontend/messages/en.json +648 -454
  45. package/hedhog/frontend/messages/pt.json +647 -454
  46. package/hedhog/table/operations_approval.yaml +49 -0
  47. package/hedhog/table/operations_approval_history.yaml +29 -0
  48. package/hedhog/table/{operations_employee.yaml → operations_collaborator.yaml} +67 -64
  49. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -0
  50. package/hedhog/table/operations_contract.yaml +100 -48
  51. package/hedhog/table/operations_contract_document.yaml +39 -0
  52. package/hedhog/table/operations_contract_financial_term.yaml +40 -0
  53. package/hedhog/table/operations_contract_history.yaml +27 -0
  54. package/hedhog/table/operations_contract_party.yaml +46 -0
  55. package/hedhog/table/operations_contract_revision.yaml +38 -0
  56. package/hedhog/table/operations_contract_signature.yaml +38 -0
  57. package/hedhog/table/operations_project.yaml +54 -50
  58. package/hedhog/table/{operations_allocation.yaml → operations_project_assignment.yaml} +55 -52
  59. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -0
  60. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -0
  61. package/hedhog/table/operations_time_off_request.yaml +57 -0
  62. package/hedhog/table/operations_timesheet.yaml +41 -36
  63. package/hedhog/table/operations_timesheet_entry.yaml +40 -50
  64. package/package.json +8 -7
  65. package/src/operations.controller.ts +182 -0
  66. package/src/operations.module.ts +22 -21
  67. package/src/operations.service.ts +3595 -137
  68. package/hedhog/data/operations_career_level.yaml +0 -102
  69. package/hedhog/data/operations_career_track.yaml +0 -8
  70. package/hedhog/data/operations_certification.yaml +0 -38
  71. package/hedhog/data/operations_evaluation_cycle.yaml +0 -18
  72. package/hedhog/data/operations_performance_criterion.yaml +0 -48
  73. package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +0 -56
  74. package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +0 -626
  75. package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +0 -142
  76. package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +0 -41
  77. package/hedhog/frontend/app/_lib/hooks/use-operations-growth-data.ts.ejs +0 -63
  78. package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +0 -74
  79. package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +0 -74
  80. package/hedhog/frontend/app/_lib/mocks/operations-growth.mock.ts.ejs +0 -824
  81. package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +0 -455
  82. package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +0 -117
  83. package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +0 -84
  84. package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +0 -67
  85. package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +0 -10
  86. package/hedhog/frontend/app/_lib/services/operations-growth.service.ts.ejs +0 -31
  87. package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +0 -10
  88. package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +0 -10
  89. package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +0 -10
  90. package/hedhog/frontend/app/_lib/types/operations-growth.ts.ejs +0 -209
  91. package/hedhog/frontend/app/_lib/types/operations.ts.ejs +0 -156
  92. package/hedhog/frontend/app/_lib/utils/growth.ts.ejs +0 -62
  93. package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +0 -103
  94. package/hedhog/frontend/app/_lib/utils/status.ts.ejs +0 -80
  95. package/hedhog/frontend/app/allocations/page.tsx.ejs +0 -155
  96. package/hedhog/frontend/app/career/page.tsx.ejs +0 -143
  97. package/hedhog/frontend/app/certifications/page.tsx.ejs +0 -202
  98. package/hedhog/frontend/app/evaluations/page.tsx.ejs +0 -278
  99. package/hedhog/frontend/app/goals/page.tsx.ejs +0 -171
  100. package/hedhog/frontend/app/growth/page.tsx.ejs +0 -288
  101. package/hedhog/frontend/app/manager/page.tsx.ejs +0 -175
  102. package/hedhog/frontend/app/rewards/page.tsx.ejs +0 -196
  103. package/hedhog/frontend/app/tasks/page.tsx.ejs +0 -999
  104. package/hedhog/table/operations_calibration_item.yaml +0 -61
  105. package/hedhog/table/operations_calibration_session.yaml +0 -25
  106. package/hedhog/table/operations_career_level.yaml +0 -75
  107. package/hedhog/table/operations_career_track.yaml +0 -21
  108. package/hedhog/table/operations_certification.yaml +0 -48
  109. package/hedhog/table/operations_employee_certification.yaml +0 -43
  110. package/hedhog/table/operations_employee_connect.yaml +0 -61
  111. package/hedhog/table/operations_employee_evaluation.yaml +0 -113
  112. package/hedhog/table/operations_employee_evaluation_item.yaml +0 -39
  113. package/hedhog/table/operations_employee_profile.yaml +0 -80
  114. package/hedhog/table/operations_employee_skill_matrix.yaml +0 -30
  115. package/hedhog/table/operations_evaluation_cycle.yaml +0 -31
  116. package/hedhog/table/operations_goal.yaml +0 -67
  117. package/hedhog/table/operations_goal_progress.yaml +0 -31
  118. package/hedhog/table/operations_performance_criterion.yaml +0 -29
  119. package/hedhog/table/operations_promotion_readiness.yaml +0 -49
  120. package/hedhog/table/operations_promotion_recommendation.yaml +0 -63
  121. package/hedhog/table/operations_public_recognition.yaml +0 -46
  122. package/hedhog/table/operations_reward.yaml +0 -100
  123. package/hedhog/table/operations_score_event.yaml +0 -81
  124. package/hedhog/table/operations_task.yaml +0 -60
  125. package/src/operations-data.controller.ts +0 -54
  126. package/src/operations-growth.controller.ts +0 -44
@@ -1,626 +0,0 @@
1
- 'use client';
2
-
3
- import { Badge } from '@/components/ui/badge';
4
- import { Button } from '@/components/ui/button';
5
- import { cn } from '@/lib/utils';
6
- import {
7
- DndContext,
8
- DragEndEvent,
9
- DragOverlay,
10
- DragStartEvent,
11
- KeyboardSensor,
12
- PointerSensor,
13
- closestCorners,
14
- useDroppable,
15
- useSensor,
16
- useSensors,
17
- } from '@dnd-kit/core';
18
- import {
19
- SortableContext,
20
- sortableKeyboardCoordinates,
21
- useSortable,
22
- verticalListSortingStrategy,
23
- } from '@dnd-kit/sortable';
24
- import { CSS } from '@dnd-kit/utilities';
25
- import {
26
- AlertCircle,
27
- CalendarClock,
28
- ChevronDown,
29
- ChevronRight,
30
- Clock3,
31
- UserRound,
32
- } from 'lucide-react';
33
- import { memo, useMemo, useState } from 'react';
34
- import type {
35
- OperationsUser,
36
- Task,
37
- TaskPriority,
38
- TaskStatus,
39
- } from '../_lib/types/operations';
40
- import { getTaskBadgeClasses, getTaskStatusLabel } from '../_lib/utils/status';
41
- import { StatusBadge } from './status-badge';
42
-
43
- export const TASK_UNASSIGNED_LANE = 'unassigned';
44
-
45
- const columns: TaskStatus[] = [
46
- 'backlog',
47
- 'todo',
48
- 'in-progress',
49
- 'review',
50
- 'done',
51
- ];
52
-
53
- type Lane = {
54
- id: string;
55
- name: string;
56
- role?: string;
57
- };
58
-
59
- type ColumnMeta = {
60
- laneId: string;
61
- status: TaskStatus;
62
- };
63
-
64
- export interface MoveTasksPayload {
65
- draggedTaskId: string;
66
- toLaneId: string;
67
- toStatus: TaskStatus;
68
- beforeTaskId?: string;
69
- }
70
-
71
- interface KanbanBoardProps {
72
- tasks: Task[];
73
- users: OperationsUser[];
74
- selectedTaskIds?: string[];
75
- focusedTaskId?: string | null;
76
- keyboardDragMode?: boolean;
77
- collapsedLaneIds?: string[];
78
- onToggleLane?: (laneId: string) => void;
79
- onTaskFocus?: (taskId: string) => void;
80
- onTaskOpen?: (taskId: string) => void;
81
- onSelectionChange?: (taskId: string, multi: boolean) => void;
82
- onMoveTasks?: (payload: MoveTasksPayload) => void;
83
- }
84
-
85
- function getLaneId(task: Task) {
86
- return task.assignedUserId || TASK_UNASSIGNED_LANE;
87
- }
88
-
89
- function getColumnId(laneId: string, status: TaskStatus) {
90
- return `col:${laneId}:${status}`;
91
- }
92
-
93
- function parseColumnId(columnId: string): ColumnMeta | null {
94
- if (!columnId.startsWith('col:')) {
95
- return null;
96
- }
97
-
98
- const [, laneId, status] = columnId.split(':');
99
-
100
- if (!laneId || !status) {
101
- return null;
102
- }
103
-
104
- if (!columns.includes(status as TaskStatus)) {
105
- return null;
106
- }
107
-
108
- return {
109
- laneId,
110
- status: status as TaskStatus,
111
- };
112
- }
113
-
114
- function getPriorityClass(priority: TaskPriority) {
115
- return {
116
- low: 'bg-emerald-100 text-emerald-700',
117
- medium: 'bg-slate-200 text-slate-700',
118
- high: 'bg-orange-100 text-orange-700',
119
- critical: 'bg-rose-100 text-rose-700',
120
- }[priority];
121
- }
122
-
123
- function formatDueDate(dueDate: string) {
124
- const date = new Date(`${dueDate}T00:00:00`);
125
-
126
- if (Number.isNaN(date.getTime())) {
127
- return dueDate;
128
- }
129
-
130
- return new Intl.DateTimeFormat('en-US', {
131
- month: 'short',
132
- day: 'numeric',
133
- }).format(date);
134
- }
135
-
136
- const TaskCard = memo(function TaskCard({
137
- task,
138
- selected,
139
- focused,
140
- userName,
141
- keyboardDragMode,
142
- onFocus,
143
- onOpen,
144
- onSelect,
145
- }: {
146
- task: Task;
147
- selected: boolean;
148
- focused: boolean;
149
- userName: string;
150
- keyboardDragMode: boolean;
151
- onFocus: (taskId: string) => void;
152
- onOpen: (taskId: string) => void;
153
- onSelect: (taskId: string, multi: boolean) => void;
154
- }) {
155
- const sortable = useSortable({
156
- id: task.id,
157
- data: {
158
- type: 'task',
159
- laneId: getLaneId(task),
160
- status: task.status,
161
- },
162
- disabled: false,
163
- });
164
-
165
- const style = {
166
- transform: CSS.Transform.toString(sortable.transform),
167
- transition: sortable.transition,
168
- };
169
-
170
- return (
171
- <article
172
- ref={sortable.setNodeRef}
173
- style={style}
174
- aria-label={`Task ${task.title}`}
175
- onFocus={() => onFocus(task.id)}
176
- onClick={(event) => {
177
- const useMulti = event.ctrlKey || event.metaKey;
178
- onSelect(task.id, useMulti);
179
- }}
180
- onDoubleClick={() => onOpen(task.id)}
181
- onKeyDown={(event) => {
182
- if (event.key === 'Enter') {
183
- event.preventDefault();
184
- onOpen(task.id);
185
- return;
186
- }
187
-
188
- if (
189
- event.key.toLowerCase() === 'a' &&
190
- (event.ctrlKey || event.metaKey)
191
- ) {
192
- event.preventDefault();
193
- onSelect(task.id, true);
194
- }
195
- }}
196
- className={cn(
197
- 'rounded-xl border bg-background p-3 shadow-xs outline-hidden transition',
198
- 'focus-visible:ring-2 focus-visible:ring-primary/40',
199
- selected && 'border-primary bg-primary/5',
200
- focused && 'ring-2 ring-primary/40',
201
- sortable.isDragging && 'opacity-40',
202
- sortable.isOver && 'border-primary/60'
203
- )}
204
- {...sortable.attributes}
205
- {...sortable.listeners}
206
- >
207
- <div className="mb-2 flex items-start justify-between gap-2">
208
- <p className="line-clamp-2 text-sm font-semibold">{task.title}</p>
209
- <StatusBadge
210
- label={getTaskStatusLabel(task.status)}
211
- className={getTaskBadgeClasses(task.status)}
212
- />
213
- </div>
214
- <div className="mb-2 flex flex-wrap gap-1.5">
215
- <Badge
216
- className={cn('border-transparent', getPriorityClass(task.priority))}
217
- >
218
- {task.priority}
219
- </Badge>
220
- {task.labels.map((label) => (
221
- <Badge
222
- key={label}
223
- variant="secondary"
224
- className="bg-slate-100 text-[11px] text-slate-700"
225
- >
226
- {label}
227
- </Badge>
228
- ))}
229
- </div>
230
- <div className="grid gap-1 text-xs text-muted-foreground">
231
- <div className="flex items-center gap-1.5">
232
- <UserRound className="size-3.5" />
233
- <span>{userName}</span>
234
- </div>
235
- <div className="flex items-center gap-1.5">
236
- <CalendarClock className="size-3.5" />
237
- <span>{formatDueDate(task.dueDate)}</span>
238
- </div>
239
- <div className="flex items-center gap-1.5">
240
- <Clock3 className="size-3.5" />
241
- <span>{task.estimatedHours}h</span>
242
- {keyboardDragMode ? (
243
- <span className="ml-auto inline-flex items-center gap-1 text-[10px] uppercase text-primary">
244
- <AlertCircle className="size-3" />
245
- KBD drag
246
- </span>
247
- ) : null}
248
- </div>
249
- </div>
250
- </article>
251
- );
252
- });
253
-
254
- const KanbanColumn = memo(function KanbanColumn({
255
- laneId,
256
- status,
257
- tasks,
258
- selectedTaskIds,
259
- focusedTaskId,
260
- usersById,
261
- keyboardDragMode,
262
- onTaskFocus,
263
- onTaskOpen,
264
- onSelectionChange,
265
- }: {
266
- laneId: string;
267
- status: TaskStatus;
268
- tasks: Task[];
269
- selectedTaskIds: Set<string>;
270
- focusedTaskId: string | null;
271
- usersById: Map<string, OperationsUser>;
272
- keyboardDragMode: boolean;
273
- onTaskFocus: (taskId: string) => void;
274
- onTaskOpen: (taskId: string) => void;
275
- onSelectionChange: (taskId: string, multi: boolean) => void;
276
- }) {
277
- const columnId = getColumnId(laneId, status);
278
-
279
- const droppable = useDroppable({
280
- id: columnId,
281
- data: {
282
- type: 'column',
283
- laneId,
284
- status,
285
- },
286
- });
287
-
288
- return (
289
- <section
290
- ref={droppable.setNodeRef}
291
- className={cn(
292
- 'rounded-xl border bg-slate-50/80 p-3',
293
- droppable.isOver && 'border-primary/70 bg-primary/5'
294
- )}
295
- aria-label={`${getTaskStatusLabel(status)} column`}
296
- >
297
- <header className="mb-3 flex items-center justify-between gap-2">
298
- <h3 className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
299
- {getTaskStatusLabel(status)}
300
- </h3>
301
- <span className="rounded-md bg-background px-2 py-0.5 text-xs font-medium text-muted-foreground">
302
- {tasks.length}
303
- </span>
304
- </header>
305
-
306
- <SortableContext
307
- items={tasks.map((task) => task.id)}
308
- strategy={verticalListSortingStrategy}
309
- >
310
- <div className="space-y-2.5">
311
- {tasks.map((task) => (
312
- <TaskCard
313
- key={task.id}
314
- task={task}
315
- selected={selectedTaskIds.has(task.id)}
316
- focused={focusedTaskId === task.id}
317
- userName={
318
- usersById.get(task.assignedUserId)?.name || 'Unassigned'
319
- }
320
- keyboardDragMode={keyboardDragMode}
321
- onFocus={onTaskFocus}
322
- onOpen={onTaskOpen}
323
- onSelect={onSelectionChange}
324
- />
325
- ))}
326
- </div>
327
- </SortableContext>
328
- </section>
329
- );
330
- });
331
-
332
- const LaneSection = memo(function LaneSection({
333
- lane,
334
- columnsByStatus,
335
- collapsed,
336
- onToggle,
337
- selectedTaskIds,
338
- focusedTaskId,
339
- usersById,
340
- keyboardDragMode,
341
- onTaskFocus,
342
- onTaskOpen,
343
- onSelectionChange,
344
- }: {
345
- lane: Lane;
346
- columnsByStatus: Record<TaskStatus, Task[]>;
347
- collapsed: boolean;
348
- onToggle: (laneId: string) => void;
349
- selectedTaskIds: Set<string>;
350
- focusedTaskId: string | null;
351
- usersById: Map<string, OperationsUser>;
352
- keyboardDragMode: boolean;
353
- onTaskFocus: (taskId: string) => void;
354
- onTaskOpen: (taskId: string) => void;
355
- onSelectionChange: (taskId: string, multi: boolean) => void;
356
- }) {
357
- const total = columns.reduce(
358
- (acc, status) => acc + columnsByStatus[status].length,
359
- 0
360
- );
361
-
362
- return (
363
- <div className="rounded-2xl border bg-background p-3 shadow-xs">
364
- <button
365
- type="button"
366
- onClick={() => onToggle(lane.id)}
367
- className="mb-3 flex w-full items-center justify-between rounded-lg px-2 py-1.5 text-left transition hover:bg-muted/60"
368
- >
369
- <span>
370
- <span className="text-sm font-semibold">{lane.name}</span>
371
- {lane.role ? (
372
- <span className="ml-2 text-xs text-muted-foreground">
373
- {lane.role}
374
- </span>
375
- ) : null}
376
- </span>
377
- <span className="inline-flex items-center gap-2 text-xs text-muted-foreground">
378
- {total} cards
379
- {collapsed ? (
380
- <ChevronRight className="size-4" />
381
- ) : (
382
- <ChevronDown className="size-4" />
383
- )}
384
- </span>
385
- </button>
386
-
387
- {collapsed ? null : (
388
- <div className="overflow-x-auto pb-1">
389
- <div className="grid min-w-[1120px] grid-cols-5 gap-3">
390
- {columns.map((status) => (
391
- <KanbanColumn
392
- key={`${lane.id}-${status}`}
393
- laneId={lane.id}
394
- status={status}
395
- tasks={columnsByStatus[status]}
396
- selectedTaskIds={selectedTaskIds}
397
- focusedTaskId={focusedTaskId}
398
- usersById={usersById}
399
- keyboardDragMode={keyboardDragMode}
400
- onTaskFocus={onTaskFocus}
401
- onTaskOpen={onTaskOpen}
402
- onSelectionChange={onSelectionChange}
403
- />
404
- ))}
405
- </div>
406
- </div>
407
- )}
408
- </div>
409
- );
410
- });
411
-
412
- export function KanbanBoard({
413
- tasks,
414
- users,
415
- selectedTaskIds = [],
416
- focusedTaskId = null,
417
- keyboardDragMode = false,
418
- collapsedLaneIds = [],
419
- onToggleLane = () => {},
420
- onTaskFocus = () => {},
421
- onTaskOpen = () => {},
422
- onSelectionChange = () => {},
423
- onMoveTasks = () => {},
424
- }: KanbanBoardProps) {
425
- const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
426
-
427
- const selectedSet = useMemo(
428
- () => new Set(selectedTaskIds),
429
- [selectedTaskIds]
430
- );
431
-
432
- const usersById = useMemo(
433
- () => new Map(users.map((user) => [user.id, user])),
434
- [users]
435
- );
436
-
437
- const lanes = useMemo(() => {
438
- const byId = new Map<string, Lane>();
439
-
440
- users.forEach((user) => {
441
- byId.set(user.id, {
442
- id: user.id,
443
- name: user.name,
444
- role: user.role,
445
- });
446
- });
447
-
448
- if (tasks.some((task) => !task.assignedUserId)) {
449
- byId.set(TASK_UNASSIGNED_LANE, {
450
- id: TASK_UNASSIGNED_LANE,
451
- name: 'Unassigned',
452
- });
453
- }
454
-
455
- tasks.forEach((task) => {
456
- const laneId = getLaneId(task);
457
- if (!byId.has(laneId)) {
458
- byId.set(laneId, {
459
- id: laneId,
460
- name: laneId,
461
- });
462
- }
463
- });
464
-
465
- return Array.from(byId.values());
466
- }, [tasks, users]);
467
-
468
- const tasksByLaneAndStatus = useMemo(() => {
469
- const map = new Map<string, Record<TaskStatus, Task[]>>();
470
-
471
- lanes.forEach((lane) => {
472
- map.set(lane.id, {
473
- backlog: [],
474
- todo: [],
475
- 'in-progress': [],
476
- review: [],
477
- done: [],
478
- });
479
- });
480
-
481
- tasks.forEach((task) => {
482
- const laneId = getLaneId(task);
483
-
484
- if (!map.has(laneId)) {
485
- map.set(laneId, {
486
- backlog: [],
487
- todo: [],
488
- 'in-progress': [],
489
- review: [],
490
- done: [],
491
- });
492
- }
493
-
494
- map.get(laneId)?.[task.status].push(task);
495
- });
496
-
497
- for (const laneTasks of map.values()) {
498
- columns.forEach((status) => {
499
- laneTasks[status].sort((a, b) => a.order - b.order);
500
- });
501
- }
502
-
503
- return map;
504
- }, [lanes, tasks]);
505
-
506
- const tasksById = useMemo(
507
- () => new Map(tasks.map((task) => [task.id, task])),
508
- [tasks]
509
- );
510
-
511
- const sensors = useSensors(
512
- useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
513
- ...(keyboardDragMode
514
- ? [
515
- useSensor(KeyboardSensor, {
516
- coordinateGetter: sortableKeyboardCoordinates,
517
- }),
518
- ]
519
- : [])
520
- );
521
-
522
- const handleDragStart = (event: DragStartEvent) => {
523
- if (typeof event.active.id === 'string') {
524
- setActiveTaskId(event.active.id);
525
- onTaskFocus(event.active.id);
526
- }
527
- };
528
-
529
- const handleDragEnd = (event: DragEndEvent) => {
530
- const draggedTaskId =
531
- typeof event.active.id === 'string' ? event.active.id : null;
532
- const overId = typeof event.over?.id === 'string' ? event.over.id : null;
533
-
534
- setActiveTaskId(null);
535
-
536
- if (!draggedTaskId || !overId) {
537
- return;
538
- }
539
-
540
- if (draggedTaskId === overId) {
541
- return;
542
- }
543
-
544
- const overColumn = parseColumnId(overId);
545
-
546
- if (overColumn) {
547
- onMoveTasks({
548
- draggedTaskId,
549
- toLaneId: overColumn.laneId,
550
- toStatus: overColumn.status,
551
- });
552
- return;
553
- }
554
-
555
- const overTask = tasksById.get(overId);
556
-
557
- if (!overTask) {
558
- return;
559
- }
560
-
561
- onMoveTasks({
562
- draggedTaskId,
563
- toLaneId: getLaneId(overTask),
564
- toStatus: overTask.status,
565
- beforeTaskId: overTask.id,
566
- });
567
- };
568
-
569
- return (
570
- <DndContext
571
- sensors={sensors}
572
- collisionDetection={closestCorners}
573
- onDragStart={handleDragStart}
574
- onDragEnd={handleDragEnd}
575
- >
576
- <div className="space-y-4">
577
- {lanes.map((lane) => {
578
- const columnsByStatus = tasksByLaneAndStatus.get(lane.id);
579
-
580
- if (!columnsByStatus) {
581
- return null;
582
- }
583
-
584
- return (
585
- <LaneSection
586
- key={lane.id}
587
- lane={lane}
588
- columnsByStatus={columnsByStatus}
589
- collapsed={collapsedLaneIds.includes(lane.id)}
590
- onToggle={onToggleLane}
591
- selectedTaskIds={selectedSet}
592
- focusedTaskId={focusedTaskId}
593
- usersById={usersById}
594
- keyboardDragMode={keyboardDragMode}
595
- onTaskFocus={onTaskFocus}
596
- onTaskOpen={onTaskOpen}
597
- onSelectionChange={onSelectionChange}
598
- />
599
- );
600
- })}
601
- </div>
602
-
603
- <DragOverlay>
604
- {activeTaskId ? (
605
- <div className="w-[280px] rounded-xl border bg-background p-3 shadow-lg">
606
- <p className="mb-1 text-sm font-semibold">
607
- {tasksById.get(activeTaskId)?.title}
608
- </p>
609
- <p className="text-xs text-muted-foreground">
610
- {tasksById.get(activeTaskId)?.projectName}
611
- </p>
612
- </div>
613
- ) : null}
614
- </DragOverlay>
615
-
616
- {!tasks.length ? (
617
- <div className="rounded-xl border border-dashed border-slate-300 p-6 text-center">
618
- <p className="text-sm text-muted-foreground">No tasks to display.</p>
619
- <Button variant="outline" className="mt-3" size="sm">
620
- Adjust filters
621
- </Button>
622
- </div>
623
- ) : null}
624
- </DndContext>
625
- );
626
- }