@hed-hog/operations 0.0.331 → 0.0.332
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controllers/operations-collaborators.controller.d.ts +54 -0
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +100 -0
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
- package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-invoice.dto.js +55 -0
- package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
- package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-payment.dto.js +50 -0
- package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
- package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-invoice.dto.js +8 -0
- package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-payment.dto.js +8 -0
- package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
- package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-invoice.dto.js +9 -0
- package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-payment.dto.js +9 -0
- package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
- package/dist/operations.service.d.ts +76 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +235 -5
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/menu.yaml +27 -8
- package/hedhog/data/route.yaml +72 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +39 -3
- package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
- package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
- package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +86 -87
- package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +218 -10
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +710 -26
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +158 -38
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +807 -803
- package/hedhog/frontend/app/_lib/api.ts.ejs +631 -480
- package/hedhog/frontend/app/_lib/types.ts.ejs +6 -5
- package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
- package/hedhog/frontend/app/my-projects/page.tsx.ejs +16 -2
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +95 -157
- package/hedhog/frontend/app/projects/page.tsx.ejs +42 -6
- package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
- package/hedhog/frontend/messages/en.json +96 -2
- package/hedhog/frontend/messages/pt.json +96 -2
- package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
- package/hedhog/table/operations_collaborator_payment.yaml +32 -0
- package/package.json +5 -5
- package/src/controllers/operations-collaborators.controller.ts +117 -8
- package/src/dto/create-collaborator-invoice.dto.ts +39 -0
- package/src/dto/create-collaborator-payment.dto.ts +35 -0
- package/src/dto/list-collaborator-invoice.dto.ts +3 -0
- package/src/dto/list-collaborator-payment.dto.ts +3 -0
- package/src/dto/update-collaborator-invoice.dto.ts +6 -0
- package/src/dto/update-collaborator-payment.dto.ts +6 -0
- package/src/operations.service.ts +328 -5
|
@@ -414,11 +414,11 @@ export type OperationsTaskOption = {
|
|
|
414
414
|
projectId: number;
|
|
415
415
|
projectAssignmentId: number | null;
|
|
416
416
|
projectName: string;
|
|
417
|
-
projectCode?: string | null;
|
|
418
|
-
dueDate?: string | null;
|
|
419
|
-
estimateHours?: number | null;
|
|
420
|
-
tags?: string | null;
|
|
421
|
-
assigneeName?: string | null;
|
|
417
|
+
projectCode?: string | null;
|
|
418
|
+
dueDate?: string | null;
|
|
419
|
+
estimateHours?: number | null;
|
|
420
|
+
tags?: string | null;
|
|
421
|
+
assigneeName?: string | null;
|
|
422
422
|
assigneeUserPhotoId?: number | null;
|
|
423
423
|
assigneePersonAvatarId?: number | null;
|
|
424
424
|
commentCount?: number | null;
|
|
@@ -435,6 +435,7 @@ export type OperationsProject = {
|
|
|
435
435
|
managerCollaboratorId?: number | null;
|
|
436
436
|
clientPersonId?: number | null;
|
|
437
437
|
clientAvatarId?: number | null;
|
|
438
|
+
clientUserPhotoId?: number | null;
|
|
438
439
|
code: string;
|
|
439
440
|
name: string;
|
|
440
441
|
clientName?: string | null;
|
|
@@ -41,3 +41,21 @@ export function formatDurationMinutes(minutes?: number | null) {
|
|
|
41
41
|
if (remainingMinutes <= 0) return `${hours}h`;
|
|
42
42
|
return `${hours}h ${remainingMinutes}min`;
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
export function getTaskDescriptionPreview(value?: string | null) {
|
|
46
|
+
if (!value) return '';
|
|
47
|
+
|
|
48
|
+
return String(value)
|
|
49
|
+
.replace(/<br\s*\/?>/gi, ' ')
|
|
50
|
+
.replace(/<\/(p|div|li|ul|ol|h1|h2|h3|h4|h5|h6)>/gi, ' ')
|
|
51
|
+
.replace(/<li[^>]*>/gi, ' ')
|
|
52
|
+
.replace(/<[^>]*>/g, '')
|
|
53
|
+
.replace(/ /gi, ' ')
|
|
54
|
+
.replace(/&/gi, '&')
|
|
55
|
+
.replace(/</gi, '<')
|
|
56
|
+
.replace(/>/gi, '>')
|
|
57
|
+
.replace(/'/gi, "'")
|
|
58
|
+
.replace(/"/gi, '"')
|
|
59
|
+
.replace(/\s+/g, ' ')
|
|
60
|
+
.trim();
|
|
61
|
+
}
|
|
@@ -49,6 +49,12 @@ function getPersonAvatarUrl(avatarId?: number | null): string {
|
|
|
49
49
|
: '/placeholder.png';
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function getUserPhotoUrl(photoId?: number | null): string {
|
|
53
|
+
return typeof photoId === 'number' && photoId > 0
|
|
54
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${photoId}`
|
|
55
|
+
: '/placeholder.png';
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
function getInitials(value?: string | null): string {
|
|
53
59
|
if (!value) return '?';
|
|
54
60
|
return value
|
|
@@ -293,7 +299,11 @@ export default function OperationsMyProjectsPage() {
|
|
|
293
299
|
</span>
|
|
294
300
|
<Avatar className="h-5 w-5 shrink-0">
|
|
295
301
|
<AvatarImage
|
|
296
|
-
src={
|
|
302
|
+
src={
|
|
303
|
+
project.clientUserPhotoId
|
|
304
|
+
? getUserPhotoUrl(project.clientUserPhotoId)
|
|
305
|
+
: getPersonAvatarUrl(project.clientAvatarId)
|
|
306
|
+
}
|
|
297
307
|
alt={project.clientName ?? ''}
|
|
298
308
|
/>
|
|
299
309
|
<AvatarFallback className="text-[9px] font-medium">
|
|
@@ -402,7 +412,11 @@ export default function OperationsMyProjectsPage() {
|
|
|
402
412
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
403
413
|
<Avatar className="h-6 w-6 shrink-0">
|
|
404
414
|
<AvatarImage
|
|
405
|
-
src={
|
|
415
|
+
src={
|
|
416
|
+
project.clientUserPhotoId
|
|
417
|
+
? getUserPhotoUrl(project.clientUserPhotoId)
|
|
418
|
+
: getPersonAvatarUrl(project.clientAvatarId)
|
|
419
|
+
}
|
|
406
420
|
alt={project.clientName ?? ''}
|
|
407
421
|
/>
|
|
408
422
|
<AvatarFallback className="text-[9px] font-medium">
|
|
@@ -6,44 +6,45 @@ import {
|
|
|
6
6
|
PaginationFooter,
|
|
7
7
|
SearchBar,
|
|
8
8
|
} from '@/components/entity-list';
|
|
9
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
9
10
|
import { Button } from '@/components/ui/button';
|
|
10
11
|
import { Card, CardContent } from '@/components/ui/card';
|
|
11
|
-
import {
|
|
12
|
-
Dialog,
|
|
13
|
-
DialogContent,
|
|
14
|
-
DialogDescription,
|
|
12
|
+
import {
|
|
13
|
+
Dialog,
|
|
14
|
+
DialogContent,
|
|
15
|
+
DialogDescription,
|
|
15
16
|
DialogFooter,
|
|
16
17
|
DialogHeader,
|
|
17
|
-
DialogTitle,
|
|
18
|
-
} from '@/components/ui/dialog';
|
|
19
|
-
import { Input } from '@/components/ui/input';
|
|
20
|
-
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
21
|
-
import { Label } from '@/components/ui/label';
|
|
22
|
-
import { Progress } from '@/components/ui/progress';
|
|
23
|
-
import {
|
|
24
|
-
Select,
|
|
25
|
-
SelectContent,
|
|
26
|
-
SelectItem,
|
|
27
|
-
SelectTrigger,
|
|
28
|
-
SelectValue,
|
|
29
|
-
} from '@/components/ui/select';
|
|
30
|
-
import {
|
|
31
|
-
Sheet,
|
|
32
|
-
SheetContent,
|
|
33
|
-
SheetHeader,
|
|
34
|
-
SheetTitle,
|
|
35
|
-
} from '@/components/ui/sheet';
|
|
36
|
-
import {
|
|
37
|
-
Table,
|
|
38
|
-
TableBody,
|
|
39
|
-
TableCell,
|
|
18
|
+
DialogTitle,
|
|
19
|
+
} from '@/components/ui/dialog';
|
|
20
|
+
import { Input } from '@/components/ui/input';
|
|
21
|
+
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
22
|
+
import { Label } from '@/components/ui/label';
|
|
23
|
+
import { Progress } from '@/components/ui/progress';
|
|
24
|
+
import {
|
|
25
|
+
Select,
|
|
26
|
+
SelectContent,
|
|
27
|
+
SelectItem,
|
|
28
|
+
SelectTrigger,
|
|
29
|
+
SelectValue,
|
|
30
|
+
} from '@/components/ui/select';
|
|
31
|
+
import {
|
|
32
|
+
Sheet,
|
|
33
|
+
SheetContent,
|
|
34
|
+
SheetHeader,
|
|
35
|
+
SheetTitle,
|
|
36
|
+
} from '@/components/ui/sheet';
|
|
37
|
+
import {
|
|
38
|
+
Table,
|
|
39
|
+
TableBody,
|
|
40
|
+
TableCell,
|
|
40
41
|
TableHead,
|
|
41
42
|
TableHeader,
|
|
42
|
-
TableRow,
|
|
43
|
-
} from '@/components/ui/table';
|
|
44
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
45
|
-
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
46
|
-
import {
|
|
43
|
+
TableRow,
|
|
44
|
+
} from '@/components/ui/table';
|
|
45
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
46
|
+
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
47
|
+
import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
|
|
47
48
|
import {
|
|
48
49
|
closestCenter,
|
|
49
50
|
DndContext,
|
|
@@ -85,16 +86,15 @@ import Link from 'next/link';
|
|
|
85
86
|
import { useCallback, useMemo, useState } from 'react';
|
|
86
87
|
import { OperationsHeader } from '../_components/operations-header';
|
|
87
88
|
import { StatusBadge } from '../_components/status-badge';
|
|
88
|
-
import {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
type TaskDetailSheetData,
|
|
92
|
-
} from '../_components/task-detail-sheet';
|
|
93
|
-
import { TaskFileAttachments } from '../_components/task-file-attachments';
|
|
94
|
-
import { TimesheetEntryCreateSheet } from '../_components/timesheet-entry-create-sheet';
|
|
95
|
-
import { fetchOperations, mutateOperations } from '../_lib/api';
|
|
96
|
-
import { useMentionItems } from '../_lib/hooks/use-mention-items';
|
|
97
|
-
import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
|
|
89
|
+
import {
|
|
90
|
+
TaskCommentsSection,
|
|
91
|
+
TaskDetailSheet,
|
|
92
|
+
type TaskDetailSheetData,
|
|
93
|
+
} from '../_components/task-detail-sheet';
|
|
94
|
+
import { TaskFileAttachments } from '../_components/task-file-attachments';
|
|
95
|
+
import { TimesheetEntryCreateSheet } from '../_components/timesheet-entry-create-sheet';
|
|
96
|
+
import { fetchOperations, mutateOperations } from '../_lib/api';
|
|
97
|
+
import { useMentionItems } from '../_lib/hooks/use-mention-items';
|
|
98
98
|
import type {
|
|
99
99
|
OperationsCollaborator,
|
|
100
100
|
OperationsProjectOption,
|
|
@@ -102,6 +102,7 @@ import type {
|
|
|
102
102
|
PaginatedResponse,
|
|
103
103
|
} from '../_lib/types';
|
|
104
104
|
import { formatDate, getStatusBadgeClass } from '../_lib/utils/format';
|
|
105
|
+
import { getTaskDescriptionPreview } from '../_lib/utils/task-ui';
|
|
105
106
|
|
|
106
107
|
type TaskViewMode = 'table' | 'cards' | 'board';
|
|
107
108
|
|
|
@@ -168,25 +169,25 @@ function parseTaskId(value: UniqueIdentifier | null | undefined) {
|
|
|
168
169
|
return match ? Number(match[1]) : null;
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
function parseColumnId(
|
|
172
|
-
value: UniqueIdentifier | null | undefined
|
|
173
|
-
): BoardColumnId | null {
|
|
174
|
-
if (!value) return null;
|
|
175
|
-
const match = String(value).match(/^col-(.+)$/);
|
|
176
|
-
const id = match?.[1];
|
|
177
|
-
return KANBAN_COLUMNS.some((c) => c.id === id) ? (id as BoardColumnId) : null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function normalizeDateInputValue(value?: string | null) {
|
|
181
|
-
if (!value) return '';
|
|
182
|
-
const match = String(value)
|
|
183
|
-
.trim()
|
|
184
|
-
.match(/^\d{4}-\d{2}-\d{2}/);
|
|
185
|
-
return match?.[0] ?? '';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function splitTasksByColumn(tasks: OperationsTaskOption[]): BoardColumns {
|
|
189
|
-
return {
|
|
172
|
+
function parseColumnId(
|
|
173
|
+
value: UniqueIdentifier | null | undefined
|
|
174
|
+
): BoardColumnId | null {
|
|
175
|
+
if (!value) return null;
|
|
176
|
+
const match = String(value).match(/^col-(.+)$/);
|
|
177
|
+
const id = match?.[1];
|
|
178
|
+
return KANBAN_COLUMNS.some((c) => c.id === id) ? (id as BoardColumnId) : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeDateInputValue(value?: string | null) {
|
|
182
|
+
if (!value) return '';
|
|
183
|
+
const match = String(value)
|
|
184
|
+
.trim()
|
|
185
|
+
.match(/^\d{4}-\d{2}-\d{2}/);
|
|
186
|
+
return match?.[0] ?? '';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function splitTasksByColumn(tasks: OperationsTaskOption[]): BoardColumns {
|
|
190
|
+
return {
|
|
190
191
|
todo: tasks.filter((t) => t.status === 'todo'),
|
|
191
192
|
doing: tasks.filter((t) => t.status === 'doing'),
|
|
192
193
|
review: tasks.filter((t) => t.status === 'review'),
|
|
@@ -334,10 +335,10 @@ export default function OperationsMyTasksPage() {
|
|
|
334
335
|
const t = useTranslations('operations.MyTasksPage');
|
|
335
336
|
const commonT = useTranslations('operations.Common');
|
|
336
337
|
const detailT = useTranslations('operations.ProjectDetailsPage');
|
|
337
|
-
|
|
338
|
-
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
339
|
-
const mentionItems = useMentionItems(request);
|
|
340
|
-
const [search, setSearch] = useState('');
|
|
338
|
+
|
|
339
|
+
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
340
|
+
const mentionItems = useMentionItems(request);
|
|
341
|
+
const [search, setSearch] = useState('');
|
|
341
342
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
342
343
|
const [page, setPage] = useState(1);
|
|
343
344
|
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
@@ -366,11 +367,11 @@ export default function OperationsMyTasksPage() {
|
|
|
366
367
|
const [deletePromptTask, setDeletePromptTask] =
|
|
367
368
|
useState<OperationsTaskOption | null>(null);
|
|
368
369
|
const [taskFormOpen, setTaskFormOpen] = useState(false);
|
|
369
|
-
const [editingTaskId, setEditingTaskId] = useState<number | null>(null);
|
|
370
|
-
const [archivingTaskId, setArchivingTaskId] = useState<number | null>(null);
|
|
371
|
-
const [taskFormLoading, setTaskFormLoading] = useState(false);
|
|
372
|
-
const [taskFormData, setTaskFormData] =
|
|
373
|
-
useState<TaskFormState>(EMPTY_TASK_FORM);
|
|
370
|
+
const [editingTaskId, setEditingTaskId] = useState<number | null>(null);
|
|
371
|
+
const [archivingTaskId, setArchivingTaskId] = useState<number | null>(null);
|
|
372
|
+
const [taskFormLoading, setTaskFormLoading] = useState(false);
|
|
373
|
+
const [taskFormData, setTaskFormData] =
|
|
374
|
+
useState<TaskFormState>(EMPTY_TASK_FORM);
|
|
374
375
|
const [activeDragTask, setActiveDragTask] =
|
|
375
376
|
useState<OperationsTaskOption | null>(null);
|
|
376
377
|
const [isTimesheetEntrySheetOpen, setIsTimesheetEntrySheetOpen] =
|
|
@@ -705,9 +706,13 @@ export default function OperationsMyTasksPage() {
|
|
|
705
706
|
[inlineCreateName, request, refetchBoard]
|
|
706
707
|
);
|
|
707
708
|
|
|
708
|
-
const openCreateTaskForm = useCallback(() => {
|
|
709
|
+
const openCreateTaskForm = useCallback((status?: BoardColumnId) => {
|
|
709
710
|
setEditingTaskId(null);
|
|
710
711
|
setSelectedTask(null);
|
|
712
|
+
setTaskFormData((prev) => ({
|
|
713
|
+
...EMPTY_TASK_FORM,
|
|
714
|
+
...(status ? { status } : { status: prev.status }),
|
|
715
|
+
}));
|
|
711
716
|
setTaskFormOpen(true);
|
|
712
717
|
}, []);
|
|
713
718
|
|
|
@@ -933,7 +938,7 @@ export default function OperationsMyTasksPage() {
|
|
|
933
938
|
{(isOver) => (
|
|
934
939
|
<div
|
|
935
940
|
className={[
|
|
936
|
-
'flex min-h-
|
|
941
|
+
'flex min-h-48 max-h-160 flex-col rounded-3xl border bg-linear-to-b p-3 transition-all',
|
|
937
942
|
getColumnClassName(column.id),
|
|
938
943
|
isOver
|
|
939
944
|
? 'border-primary shadow-lg ring-2 ring-primary/15'
|
|
@@ -963,17 +968,14 @@ export default function OperationsMyTasksPage() {
|
|
|
963
968
|
<button
|
|
964
969
|
type="button"
|
|
965
970
|
className="flex size-5 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
966
|
-
onClick={() =>
|
|
967
|
-
setInlineCreateColumn(column.id);
|
|
968
|
-
setInlineCreateName('');
|
|
969
|
-
}}
|
|
971
|
+
onClick={() => openCreateTaskForm(column.id)}
|
|
970
972
|
>
|
|
971
973
|
<Plus className="size-3.5" />
|
|
972
974
|
</button>
|
|
973
975
|
</div>
|
|
974
976
|
</div>
|
|
975
977
|
|
|
976
|
-
<div className="flex flex-1 flex-col gap-2">
|
|
978
|
+
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto pb-1 pr-0.5">
|
|
977
979
|
<AnimatePresence initial={false}>
|
|
978
980
|
{boardColumns[column.id].map((task) => {
|
|
979
981
|
const taskTags = task.tags
|
|
@@ -1025,9 +1027,8 @@ export default function OperationsMyTasksPage() {
|
|
|
1025
1027
|
</p>
|
|
1026
1028
|
{task.description ? (
|
|
1027
1029
|
<p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
|
|
1028
|
-
{
|
|
1029
|
-
|
|
1030
|
-
''
|
|
1030
|
+
{getTaskDescriptionPreview(
|
|
1031
|
+
task.description
|
|
1031
1032
|
)}
|
|
1032
1033
|
</p>
|
|
1033
1034
|
) : null}
|
|
@@ -1151,76 +1152,14 @@ export default function OperationsMyTasksPage() {
|
|
|
1151
1152
|
</AnimatePresence>
|
|
1152
1153
|
</div>
|
|
1153
1154
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
}
|
|
1163
|
-
onKeyDown={(e) => {
|
|
1164
|
-
if (e.key === 'Enter') {
|
|
1165
|
-
e.preventDefault();
|
|
1166
|
-
void handleInlineCreateTask(column.id);
|
|
1167
|
-
} else if (e.key === 'Escape') {
|
|
1168
|
-
setInlineCreateColumn(null);
|
|
1169
|
-
setInlineCreateName('');
|
|
1170
|
-
}
|
|
1171
|
-
}}
|
|
1172
|
-
onBlur={() => {
|
|
1173
|
-
if (!inlineCreateName.trim()) {
|
|
1174
|
-
setInlineCreateColumn(null);
|
|
1175
|
-
setInlineCreateName('');
|
|
1176
|
-
}
|
|
1177
|
-
}}
|
|
1178
|
-
disabled={inlineCreateLoading}
|
|
1179
|
-
className="h-8 text-sm"
|
|
1180
|
-
/>
|
|
1181
|
-
<div className="flex gap-1">
|
|
1182
|
-
<Button
|
|
1183
|
-
type="button"
|
|
1184
|
-
size="sm"
|
|
1185
|
-
className="h-7 px-2 text-xs"
|
|
1186
|
-
disabled={
|
|
1187
|
-
!inlineCreateName.trim() || inlineCreateLoading
|
|
1188
|
-
}
|
|
1189
|
-
onMouseDown={(e) => e.preventDefault()}
|
|
1190
|
-
onClick={() =>
|
|
1191
|
-
void handleInlineCreateTask(column.id)
|
|
1192
|
-
}
|
|
1193
|
-
>
|
|
1194
|
-
{t('actions.create')}
|
|
1195
|
-
</Button>
|
|
1196
|
-
<Button
|
|
1197
|
-
type="button"
|
|
1198
|
-
variant="ghost"
|
|
1199
|
-
size="sm"
|
|
1200
|
-
className="h-7 px-2 text-xs"
|
|
1201
|
-
onMouseDown={(e) => e.preventDefault()}
|
|
1202
|
-
onClick={() => {
|
|
1203
|
-
setInlineCreateColumn(null);
|
|
1204
|
-
setInlineCreateName('');
|
|
1205
|
-
}}
|
|
1206
|
-
>
|
|
1207
|
-
{commonT('actions.cancel')}
|
|
1208
|
-
</Button>
|
|
1209
|
-
</div>
|
|
1210
|
-
</div>
|
|
1211
|
-
) : (
|
|
1212
|
-
<button
|
|
1213
|
-
type="button"
|
|
1214
|
-
className="flex w-full cursor-pointer items-center gap-1 rounded-md px-1 py-1 text-xs text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
1215
|
-
onClick={() => {
|
|
1216
|
-
setInlineCreateColumn(column.id);
|
|
1217
|
-
setInlineCreateName('');
|
|
1218
|
-
}}
|
|
1219
|
-
>
|
|
1220
|
-
<Plus className="size-3" />
|
|
1221
|
-
{t('actions.create')}
|
|
1222
|
-
</button>
|
|
1223
|
-
)}
|
|
1155
|
+
<button
|
|
1156
|
+
type="button"
|
|
1157
|
+
className="mt-auto flex w-full cursor-pointer items-center justify-center gap-1 rounded-2xl border border-dashed bg-background/70 px-3 py-2 text-xs text-muted-foreground transition hover:border-primary/40 hover:bg-primary/5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
|
1158
|
+
onClick={() => openCreateTaskForm(column.id)}
|
|
1159
|
+
>
|
|
1160
|
+
<Plus className="size-3" />
|
|
1161
|
+
{t('actions.create')}
|
|
1162
|
+
</button>
|
|
1224
1163
|
</div>
|
|
1225
1164
|
)}
|
|
1226
1165
|
</DroppableColumn>
|
|
@@ -1276,7 +1215,7 @@ export default function OperationsMyTasksPage() {
|
|
|
1276
1215
|
|
|
1277
1216
|
{task.description ? (
|
|
1278
1217
|
<p className="line-clamp-3 text-sm text-muted-foreground">
|
|
1279
|
-
{task.description}
|
|
1218
|
+
{getTaskDescriptionPreview(task.description)}
|
|
1280
1219
|
</p>
|
|
1281
1220
|
) : null}
|
|
1282
1221
|
|
|
@@ -1325,7 +1264,7 @@ export default function OperationsMyTasksPage() {
|
|
|
1325
1264
|
<div className="truncate font-medium">{task.name}</div>
|
|
1326
1265
|
{task.description ? (
|
|
1327
1266
|
<div className="truncate text-xs text-muted-foreground">
|
|
1328
|
-
{task.description}
|
|
1267
|
+
{getTaskDescriptionPreview(task.description)}
|
|
1329
1268
|
</div>
|
|
1330
1269
|
) : null}
|
|
1331
1270
|
</div>
|
|
@@ -1526,11 +1465,10 @@ export default function OperationsMyTasksPage() {
|
|
|
1526
1465
|
</DialogContent>
|
|
1527
1466
|
</Dialog>
|
|
1528
1467
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
setTaskFormOpen(open);
|
|
1468
|
+
<Sheet
|
|
1469
|
+
open={taskFormOpen}
|
|
1470
|
+
onOpenChange={(open) => {
|
|
1471
|
+
setTaskFormOpen(open);
|
|
1534
1472
|
if (!open) setEditingTaskId(null);
|
|
1535
1473
|
}}
|
|
1536
1474
|
>
|
|
@@ -194,9 +194,15 @@ function DroppableProjectColumn({
|
|
|
194
194
|
return <div ref={setNodeRef}>{children(isOver)}</div>;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
function getPersonAvatarUrl(avatarId?: number | null)
|
|
197
|
+
function getPersonAvatarUrl(avatarId?: number | null) {
|
|
198
198
|
return typeof avatarId === 'number' && avatarId > 0
|
|
199
199
|
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
|
|
200
|
+
: undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getUserPhotoUrl(photoId?: number | null) {
|
|
204
|
+
return typeof photoId === 'number' && photoId > 0
|
|
205
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${photoId}`
|
|
200
206
|
: '/placeholder.png';
|
|
201
207
|
}
|
|
202
208
|
|
|
@@ -506,9 +512,31 @@ export default function OperationsProjectsPage() {
|
|
|
506
512
|
>
|
|
507
513
|
<CardContent className="space-y-2 p-3">
|
|
508
514
|
<div className="truncate text-sm font-semibold">{project.name}</div>
|
|
509
|
-
<div className="
|
|
510
|
-
{
|
|
511
|
-
|
|
515
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
516
|
+
{project.clientName ? (
|
|
517
|
+
<>
|
|
518
|
+
<span className="truncate">{project.code || '—'}</span>
|
|
519
|
+
<span>•</span>
|
|
520
|
+
<Avatar className="h-4 w-4 shrink-0">
|
|
521
|
+
<AvatarImage
|
|
522
|
+
src={
|
|
523
|
+
project.clientUserPhotoId
|
|
524
|
+
? getUserPhotoUrl(project.clientUserPhotoId)
|
|
525
|
+
: getPersonAvatarUrl(project.clientAvatarId)
|
|
526
|
+
}
|
|
527
|
+
alt={project.clientName}
|
|
528
|
+
/>
|
|
529
|
+
<AvatarFallback className="text-[8px] font-medium">
|
|
530
|
+
{getInitials(project.clientName)}
|
|
531
|
+
</AvatarFallback>
|
|
532
|
+
</Avatar>
|
|
533
|
+
<span className="truncate">{project.clientName}</span>
|
|
534
|
+
</>
|
|
535
|
+
) : (
|
|
536
|
+
<span className="truncate">
|
|
537
|
+
{project.code || commonT('labels.notAvailable')}
|
|
538
|
+
</span>
|
|
539
|
+
)}
|
|
512
540
|
</div>
|
|
513
541
|
<div className="flex items-center justify-between gap-2 pt-1">
|
|
514
542
|
<StatusBadge
|
|
@@ -758,7 +786,11 @@ export default function OperationsProjectsPage() {
|
|
|
758
786
|
</span>
|
|
759
787
|
<Avatar className="h-5 w-5 shrink-0">
|
|
760
788
|
<AvatarImage
|
|
761
|
-
src={
|
|
789
|
+
src={
|
|
790
|
+
project.clientUserPhotoId
|
|
791
|
+
? getUserPhotoUrl(project.clientUserPhotoId)
|
|
792
|
+
: getPersonAvatarUrl(project.clientAvatarId)
|
|
793
|
+
}
|
|
762
794
|
alt={project.clientName ?? ''}
|
|
763
795
|
/>
|
|
764
796
|
<AvatarFallback className="text-[9px] font-medium">
|
|
@@ -963,7 +995,11 @@ export default function OperationsProjectsPage() {
|
|
|
963
995
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
964
996
|
<Avatar className="h-6 w-6 shrink-0">
|
|
965
997
|
<AvatarImage
|
|
966
|
-
src={
|
|
998
|
+
src={
|
|
999
|
+
project.clientUserPhotoId
|
|
1000
|
+
? getUserPhotoUrl(project.clientUserPhotoId)
|
|
1001
|
+
: getPersonAvatarUrl(project.clientAvatarId)
|
|
1002
|
+
}
|
|
967
1003
|
alt={project.clientName ?? ''}
|
|
968
1004
|
/>
|
|
969
1005
|
<AvatarFallback className="text-[9px] font-medium">
|