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