@hed-hog/operations 0.0.306 → 0.0.310

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/controllers/operations-approvals.controller.d.ts +114 -1
  2. package/dist/controllers/operations-approvals.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-approvals.controller.js +16 -3
  4. package/dist/controllers/operations-approvals.controller.js.map +1 -1
  5. package/dist/controllers/operations-collaborators.controller.d.ts +16 -1
  6. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  7. package/dist/controllers/operations-collaborators.controller.js +16 -3
  8. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  9. package/dist/controllers/operations-contracts.controller.d.ts +14 -453
  10. package/dist/controllers/operations-contracts.controller.d.ts.map +1 -1
  11. package/dist/controllers/operations-contracts.controller.js +11 -112
  12. package/dist/controllers/operations-contracts.controller.js.map +1 -1
  13. package/dist/controllers/operations-org-structure.controller.d.ts +65 -2
  14. package/dist/controllers/operations-org-structure.controller.d.ts.map +1 -1
  15. package/dist/controllers/operations-org-structure.controller.js +18 -5
  16. package/dist/controllers/operations-org-structure.controller.js.map +1 -1
  17. package/dist/controllers/operations-projects.controller.d.ts +28 -4
  18. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  19. package/dist/controllers/operations-projects.controller.js +17 -5
  20. package/dist/controllers/operations-projects.controller.js.map +1 -1
  21. package/dist/controllers/operations-timesheets.controller.d.ts +31 -4
  22. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  23. package/dist/controllers/operations-timesheets.controller.js +16 -11
  24. package/dist/controllers/operations-timesheets.controller.js.map +1 -1
  25. package/dist/dto/list-approvals.dto.d.ts +6 -0
  26. package/dist/dto/list-approvals.dto.d.ts.map +1 -0
  27. package/dist/dto/list-approvals.dto.js +28 -0
  28. package/dist/dto/list-approvals.dto.js.map +1 -0
  29. package/dist/dto/list-collaborator-types.dto.d.ts +3 -1
  30. package/dist/dto/list-collaborator-types.dto.d.ts.map +1 -1
  31. package/dist/dto/list-collaborator-types.dto.js +7 -1
  32. package/dist/dto/list-collaborator-types.dto.js.map +1 -1
  33. package/dist/dto/list-collaborators.dto.d.ts +1 -0
  34. package/dist/dto/list-collaborators.dto.d.ts.map +1 -1
  35. package/dist/dto/list-collaborators.dto.js +5 -0
  36. package/dist/dto/list-collaborators.dto.js.map +1 -1
  37. package/dist/dto/list-contracts.dto.d.ts +8 -0
  38. package/dist/dto/list-contracts.dto.d.ts.map +1 -0
  39. package/dist/dto/list-contracts.dto.js +38 -0
  40. package/dist/dto/list-contracts.dto.js.map +1 -0
  41. package/dist/dto/list-departments.dto.d.ts +5 -0
  42. package/dist/dto/list-departments.dto.d.ts.map +1 -0
  43. package/dist/dto/list-departments.dto.js +23 -0
  44. package/dist/dto/list-departments.dto.js.map +1 -0
  45. package/dist/dto/list-projects.dto.d.ts +5 -0
  46. package/dist/dto/list-projects.dto.d.ts.map +1 -0
  47. package/dist/dto/list-projects.dto.js +23 -0
  48. package/dist/dto/list-projects.dto.js.map +1 -0
  49. package/dist/dto/list-schedule-adjustments.dto.d.ts +5 -0
  50. package/dist/dto/list-schedule-adjustments.dto.d.ts.map +1 -0
  51. package/dist/dto/list-schedule-adjustments.dto.js +23 -0
  52. package/dist/dto/list-schedule-adjustments.dto.js.map +1 -0
  53. package/dist/dto/list-time-off-requests.dto.d.ts +5 -0
  54. package/dist/dto/list-time-off-requests.dto.d.ts.map +1 -0
  55. package/dist/dto/list-time-off-requests.dto.js +23 -0
  56. package/dist/dto/list-time-off-requests.dto.js.map +1 -0
  57. package/dist/dto/list-timesheets.dto.d.ts +5 -0
  58. package/dist/dto/list-timesheets.dto.d.ts.map +1 -0
  59. package/dist/dto/list-timesheets.dto.js +23 -0
  60. package/dist/dto/list-timesheets.dto.js.map +1 -0
  61. package/dist/dto/reorder-collaborator-types.dto.d.ts +4 -0
  62. package/dist/dto/reorder-collaborator-types.dto.d.ts.map +1 -0
  63. package/dist/dto/reorder-collaborator-types.dto.js +25 -0
  64. package/dist/dto/reorder-collaborator-types.dto.js.map +1 -0
  65. package/dist/operations.service.d.ts +340 -271
  66. package/dist/operations.service.d.ts.map +1 -1
  67. package/dist/operations.service.js +1007 -1043
  68. package/dist/operations.service.js.map +1 -1
  69. package/dist/operations.service.spec.js +0 -22
  70. package/dist/operations.service.spec.js.map +1 -1
  71. package/hedhog/data/menu.yaml +0 -36
  72. package/hedhog/data/route.yaml +42 -73
  73. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +8 -1
  74. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +15 -10
  75. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +108 -213
  76. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +251 -2039
  77. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +167 -60
  78. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +70 -301
  79. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +102 -51
  80. package/hedhog/frontend/app/_lib/types.ts.ejs +19 -24
  81. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +14 -9
  82. package/hedhog/frontend/app/approvals/page.tsx.ejs +842 -150
  83. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +445 -153
  84. package/hedhog/frontend/app/collaborators/page.tsx.ejs +118 -49
  85. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  86. package/hedhog/frontend/app/contracts/page.tsx.ejs +215 -617
  87. package/hedhog/frontend/app/departments/page.tsx.ejs +257 -113
  88. package/hedhog/frontend/app/projects/page.tsx.ejs +90 -51
  89. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +412 -147
  90. package/hedhog/frontend/app/time-off/page.tsx.ejs +400 -123
  91. package/hedhog/frontend/app/timesheets/page.tsx.ejs +460 -365
  92. package/hedhog/frontend/messages/en.json +143 -14
  93. package/hedhog/frontend/messages/pt.json +192 -54
  94. package/hedhog/table/operations_contract.yaml +0 -9
  95. package/package.json +4 -4
  96. package/src/controllers/operations-approvals.controller.ts +9 -3
  97. package/src/controllers/operations-collaborators.controller.ts +15 -2
  98. package/src/controllers/operations-contracts.controller.ts +8 -92
  99. package/src/controllers/operations-org-structure.controller.ts +17 -4
  100. package/src/controllers/operations-projects.controller.ts +10 -4
  101. package/src/controllers/operations-timesheets.controller.ts +17 -8
  102. package/src/dto/list-approvals.dto.ts +12 -0
  103. package/src/dto/list-collaborator-types.dto.ts +7 -2
  104. package/src/dto/list-collaborators.dto.ts +4 -0
  105. package/src/dto/list-contracts.dto.ts +20 -0
  106. package/src/dto/list-departments.dto.ts +8 -0
  107. package/src/dto/list-projects.dto.ts +8 -0
  108. package/src/dto/list-schedule-adjustments.dto.ts +8 -0
  109. package/src/dto/list-time-off-requests.dto.ts +8 -0
  110. package/src/dto/list-timesheets.dto.ts +8 -0
  111. package/src/dto/reorder-collaborator-types.dto.ts +10 -0
  112. package/src/operations.service.spec.ts +0 -30
  113. package/src/operations.service.ts +1557 -1806
  114. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +0 -631
  115. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +0 -526
  116. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +0 -247
  117. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +0 -3520
  118. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +0 -380
  119. package/hedhog/frontend/app/team/page.tsx.ejs +0 -352
  120. package/hedhog/table/operations_contract_financial_term.yaml +0 -40
  121. package/hedhog/table/operations_contract_revision.yaml +0 -38
  122. package/hedhog/table/operations_contract_signature.yaml +0 -38
  123. package/hedhog/table/operations_contract_template.yaml +0 -58
@@ -1,6 +1,11 @@
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 { Badge } from '@/components/ui/badge';
5
10
  import { Button } from '@/components/ui/button';
6
11
  import { FormActions } from '@/components/ui/form-actions';
@@ -21,9 +26,26 @@ import {
21
26
  SheetTitle,
22
27
  } from '@/components/ui/sheet';
23
28
  import { Switch } from '@/components/ui/switch';
29
+ import {
30
+ Table,
31
+ TableBody,
32
+ TableCell,
33
+ TableHead,
34
+ TableHeader,
35
+ TableRow,
36
+ } from '@/components/ui/table';
24
37
  import { Textarea } from '@/components/ui/textarea';
38
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
25
39
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
26
- import { CalendarRange, Clock, Plus, User } from 'lucide-react';
40
+ import {
41
+ CalendarRange,
42
+ Clock,
43
+ Eye,
44
+ LayoutGrid,
45
+ List,
46
+ Plus,
47
+ User,
48
+ } from 'lucide-react';
27
49
  import { useTranslations } from 'next-intl';
28
50
  import { useMemo, useState } from 'react';
29
51
  import { OperationsHeader } from '../_components/operations-header';
@@ -33,6 +55,7 @@ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
33
55
  import type {
34
56
  OperationsScheduleAdjustmentDay,
35
57
  OperationsScheduleAdjustmentRequest,
58
+ PaginatedResponse,
36
59
  } from '../_lib/types';
37
60
  import {
38
61
  formatDateRange,
@@ -211,7 +234,19 @@ export default function OperationsScheduleAdjustmentsPage() {
211
234
  const access = useOperationsAccess();
212
235
  const [search, setSearch] = useState('');
213
236
  const [statusFilter, setStatusFilter] = useState('all');
237
+ const [page, setPage] = useState(1);
238
+ const [pageSize, setPageSize] = useState(12);
239
+ const [viewMode, setViewMode] = useState<'table' | 'cards'>(() => {
240
+ if (typeof window === 'undefined') return 'cards';
241
+ const saved = window.localStorage.getItem(
242
+ 'operations-schedule-adjustments-view-mode'
243
+ );
244
+ return saved === 'table' ? 'table' : 'cards';
245
+ });
214
246
  const [isSheetOpen, setIsSheetOpen] = useState(false);
247
+ const [isDetailsOpen, setIsDetailsOpen] = useState(false);
248
+ const [selectedRequest, setSelectedRequest] =
249
+ useState<OperationsScheduleAdjustmentRequest | null>(null);
215
250
  const [form, setForm] = useState<ScheduleFormState>(emptyForm);
216
251
 
217
252
  const getRequestScopeLabel = (value?: string | null) => {
@@ -241,41 +276,44 @@ export default function OperationsScheduleAdjustmentsPage() {
241
276
  return t.has(key) ? t(key) : null;
242
277
  };
243
278
 
244
- const { data: requests = [], refetch } = useQuery<
245
- OperationsScheduleAdjustmentRequest[]
279
+ const { data: requestsResponse, refetch } = useQuery<
280
+ PaginatedResponse<OperationsScheduleAdjustmentRequest>
246
281
  >({
247
- queryKey: ['operations-schedule-adjustments', currentLocaleCode],
282
+ queryKey: [
283
+ 'operations-schedule-adjustments',
284
+ currentLocaleCode,
285
+ search,
286
+ statusFilter,
287
+ page,
288
+ pageSize,
289
+ ],
248
290
  enabled: access.isCollaborator,
249
- queryFn: () =>
250
- fetchOperations<OperationsScheduleAdjustmentRequest[]>(
251
- request,
252
- '/operations/schedule-adjustments'
253
- ),
291
+ queryFn: () => {
292
+ const params = new URLSearchParams({
293
+ page: String(page),
294
+ pageSize: String(pageSize),
295
+ });
296
+ if (search.trim()) params.set('search', search.trim());
297
+ if (statusFilter !== 'all') params.set('status', statusFilter);
298
+ return fetchOperations<
299
+ PaginatedResponse<OperationsScheduleAdjustmentRequest>
300
+ >(request, `/operations/schedule-adjustments?${params.toString()}`);
301
+ },
302
+ placeholderData: (previous) => previous,
254
303
  });
255
304
 
256
- const filteredRows = useMemo(
257
- () =>
258
- requests.filter((item) => {
259
- const matchesSearch = !search.trim()
260
- ? true
261
- : [
262
- item.collaboratorName,
263
- item.approverName,
264
- item.reason,
265
- item.requestScope,
266
- ]
267
- .filter(Boolean)
268
- .some((value) =>
269
- String(value)
270
- .toLowerCase()
271
- .includes(search.trim().toLowerCase())
272
- );
273
- const matchesStatus =
274
- statusFilter === 'all' ? true : item.status === statusFilter;
275
- return matchesSearch && matchesStatus;
276
- }),
277
- [requests, search, statusFilter]
278
- );
305
+ const requests = requestsResponse?.data ?? [];
306
+
307
+ const handleViewModeChange = (value: string) => {
308
+ if (value !== 'table' && value !== 'cards') return;
309
+ setViewMode(value as 'table' | 'cards');
310
+ if (typeof window !== 'undefined') {
311
+ window.localStorage.setItem(
312
+ 'operations-schedule-adjustments-view-mode',
313
+ value
314
+ );
315
+ }
316
+ };
279
317
 
280
318
  const cards = [
281
319
  {
@@ -370,6 +408,11 @@ export default function OperationsScheduleAdjustmentsPage() {
370
408
  }
371
409
  };
372
410
 
411
+ const openDetails = (requestItem: OperationsScheduleAdjustmentRequest) => {
412
+ setSelectedRequest(requestItem);
413
+ setIsDetailsOpen(true);
414
+ };
415
+
373
416
  return (
374
417
  <Page>
375
418
  <OperationsHeader
@@ -386,130 +429,239 @@ export default function OperationsScheduleAdjustmentsPage() {
386
429
  }
387
430
  />
388
431
 
389
- <SearchBar
390
- searchQuery={search}
391
- onSearchChange={setSearch}
392
- onSearch={() => undefined}
393
- placeholder={t('searchPlaceholder')}
394
- controls={[
395
- {
396
- id: 'status',
397
- type: 'select',
398
- value: statusFilter,
399
- onChange: setStatusFilter,
400
- placeholder: commonT('labels.status'),
401
- options: [
402
- { value: 'all', label: commonT('filters.allStatuses') },
403
- { value: 'submitted', label: getStatusLabel('submitted') },
404
- { value: 'approved', label: getStatusLabel('approved') },
405
- { value: 'rejected', label: getStatusLabel('rejected') },
406
- ],
407
- },
408
- ]}
409
- />
410
-
411
432
  <KpiCardsGrid items={cards} columns={3} />
412
433
 
413
- {filteredRows.length > 0 ? (
414
- <div className="space-y-3">
415
- {filteredRows.map((requestItem) => (
416
- <div
417
- key={requestItem.id}
418
- className="rounded-lg border bg-card shadow-sm"
419
- >
420
- {/* Card header */}
421
- <div className="flex flex-wrap items-start justify-between gap-3 border-b px-4 py-3">
422
- <div className="space-y-1">
423
- <div className="flex items-center gap-2">
424
- <User className="size-4 shrink-0 text-muted-foreground" />
425
- <span className="font-semibold">
426
- {requestItem.collaboratorName}
427
- </span>
428
- <Badge variant="outline" className="text-xs">
429
- {getRequestScopeLabel(requestItem.requestScope)}
430
- </Badge>
434
+ <div className="flex min-w-0 flex-col gap-4 xl:flex-row xl:items-center">
435
+ <div className="flex-1">
436
+ <SearchBar
437
+ searchQuery={search}
438
+ onSearchChange={(value) => {
439
+ setSearch(value);
440
+ setPage(1);
441
+ }}
442
+ showSearchButton={false}
443
+ debounceMs={500}
444
+ placeholder={t('searchPlaceholder')}
445
+ controls={[
446
+ {
447
+ id: 'status',
448
+ type: 'select',
449
+ value: statusFilter,
450
+ onChange: (value) => {
451
+ setStatusFilter(value);
452
+ setPage(1);
453
+ },
454
+ placeholder: commonT('labels.status'),
455
+ options: [
456
+ { value: 'all', label: commonT('filters.allStatuses') },
457
+ { value: 'submitted', label: getStatusLabel('submitted') },
458
+ { value: 'approved', label: getStatusLabel('approved') },
459
+ { value: 'rejected', label: getStatusLabel('rejected') },
460
+ ],
461
+ },
462
+ ]}
463
+ />
464
+ </div>
465
+
466
+ <div className="flex items-center justify-between gap-3 xl:justify-end">
467
+ <span className="text-xs font-medium text-muted-foreground">
468
+ {t('viewMode')}
469
+ </span>
470
+ <ToggleGroup
471
+ type="single"
472
+ value={viewMode}
473
+ onValueChange={handleViewModeChange}
474
+ variant="outline"
475
+ size="sm"
476
+ >
477
+ <ToggleGroupItem value="table" className="gap-1.5 px-2.5">
478
+ <List className="h-4 w-4" />
479
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
480
+ </ToggleGroupItem>
481
+ <ToggleGroupItem value="cards" className="gap-1.5 px-2.5">
482
+ <LayoutGrid className="h-4 w-4" />
483
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
484
+ </ToggleGroupItem>
485
+ </ToggleGroup>
486
+ </div>
487
+ </div>
488
+
489
+ {requests.length > 0 ? (
490
+ viewMode === 'cards' ? (
491
+ <div className="space-y-3">
492
+ {requests.map((requestItem) => (
493
+ <div
494
+ key={requestItem.id}
495
+ className="rounded-lg border bg-card shadow-sm"
496
+ >
497
+ {/* Card header */}
498
+ <div className="flex flex-wrap items-start justify-between gap-3 border-b px-4 py-3">
499
+ <div className="space-y-1">
500
+ <div className="flex items-center gap-2">
501
+ <User className="size-4 shrink-0 text-muted-foreground" />
502
+ <span className="font-semibold">
503
+ {requestItem.collaboratorName}
504
+ </span>
505
+ <Badge variant="outline" className="text-xs">
506
+ {getRequestScopeLabel(requestItem.requestScope)}
507
+ </Badge>
508
+ </div>
509
+ <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
510
+ <CalendarRange className="size-3.5 shrink-0" />
511
+ <span>
512
+ {formatDateRange(
513
+ requestItem.effectiveStartDate,
514
+ requestItem.effectiveEndDate
515
+ )}
516
+ </span>
517
+ </div>
431
518
  </div>
432
- <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
433
- <CalendarRange className="size-3.5 shrink-0" />
434
- <span>
435
- {formatDateRange(
436
- requestItem.effectiveStartDate,
437
- requestItem.effectiveEndDate
438
- )}
439
- </span>
519
+ <div className="flex flex-col items-end gap-1">
520
+ <StatusBadge
521
+ label={getStatusLabel(requestItem.status)}
522
+ className={getStatusBadgeClass(requestItem.status)}
523
+ />
524
+ {getStatusDescription(requestItem.status) ? (
525
+ <p className="text-xs text-muted-foreground">
526
+ {getStatusDescription(requestItem.status)}
527
+ </p>
528
+ ) : null}
440
529
  </div>
441
530
  </div>
442
- <div className="flex flex-col items-end gap-1">
443
- <StatusBadge
444
- label={getStatusLabel(requestItem.status)}
445
- className={getStatusBadgeClass(requestItem.status)}
446
- />
447
- {getStatusDescription(requestItem.status) ? (
448
- <p className="text-xs text-muted-foreground">
449
- {getStatusDescription(requestItem.status)}
450
- </p>
451
- ) : null}
452
- </div>
453
- </div>
454
531
 
455
- {/* Schedule comparison */}
456
- <div className="grid gap-px bg-border sm:grid-cols-2">
457
- <div className="bg-card px-4 py-3">
458
- <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
459
- {t('table.currentSchedule')}
460
- </p>
461
- <SchedulePanel
462
- days={requestItem.currentSchedule ?? []}
463
- locale={currentLocaleCode}
464
- emptyLabel={commonT('labels.notAssigned')}
465
- />
466
- </div>
467
- <div className="bg-card px-4 py-3">
468
- <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
469
- {t('table.requestedSchedule')}
470
- </p>
471
- <SchedulePanel
472
- days={
473
- (requestItem.days ??
474
- []) as OperationsScheduleAdjustmentDay[]
475
- }
476
- locale={currentLocaleCode}
477
- emptyLabel={commonT('labels.notAssigned')}
478
- />
532
+ {/* Schedule comparison */}
533
+ <div className="grid gap-px bg-border sm:grid-cols-2">
534
+ <div className="bg-card px-4 py-3">
535
+ <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
536
+ {t('table.currentSchedule')}
537
+ </p>
538
+ <SchedulePanel
539
+ days={requestItem.currentSchedule ?? []}
540
+ locale={currentLocaleCode}
541
+ emptyLabel={commonT('labels.notAssigned')}
542
+ />
543
+ </div>
544
+ <div className="bg-card px-4 py-3">
545
+ <p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
546
+ {t('table.requestedSchedule')}
547
+ </p>
548
+ <SchedulePanel
549
+ days={
550
+ (requestItem.days ??
551
+ []) as OperationsScheduleAdjustmentDay[]
552
+ }
553
+ locale={currentLocaleCode}
554
+ emptyLabel={commonT('labels.notAssigned')}
555
+ />
556
+ </div>
479
557
  </div>
480
- </div>
481
558
 
482
- {/* Card footer */}
483
- <div className="flex flex-wrap items-start justify-between gap-3 border-t bg-muted/20 px-4 py-2.5 text-sm">
484
- <div className="flex items-center gap-1.5 text-muted-foreground">
485
- <Clock className="size-3.5 shrink-0" />
486
- <span className="font-medium">
487
- {commonT('labels.approver')}:
488
- </span>
489
- <span>
490
- {requestItem.approverName || commonT('labels.notAssigned')}
491
- </span>
492
- </div>
493
- {requestItem.approverNote ? (
494
- <div className="text-xs text-muted-foreground">
495
- <span className="font-medium">
496
- {commonT('labels.notes')}:
497
- </span>{' '}
498
- {requestItem.approverNote}
499
- </div>
500
- ) : null}
501
- {requestItem.reason ? (
502
- <div className="text-xs text-muted-foreground">
503
- <span className="font-medium">
504
- {commonT('labels.reason')}:
505
- </span>{' '}
506
- {requestItem.reason}
559
+ {/* Card footer */}
560
+ <div className="flex flex-wrap items-start justify-between gap-3 border-t bg-muted/20 px-4 py-2.5 text-sm">
561
+ <div className="space-y-2">
562
+ <div className="flex items-center gap-1.5 text-muted-foreground">
563
+ <Clock className="size-3.5 shrink-0" />
564
+ <span className="font-medium">
565
+ {commonT('labels.approver')}:
566
+ </span>
567
+ <span>
568
+ {requestItem.approverName ||
569
+ commonT('labels.notAssigned')}
570
+ </span>
571
+ </div>
572
+ <div>
573
+ <Button
574
+ variant="outline"
575
+ size="sm"
576
+ className="cursor-pointer"
577
+ onClick={() => openDetails(requestItem)}
578
+ >
579
+ <Eye className="size-4" />
580
+ {t('actions.viewDetails')}
581
+ </Button>
582
+ </div>
507
583
  </div>
508
- ) : null}
584
+ {requestItem.approverNote ? (
585
+ <div className="text-xs text-muted-foreground">
586
+ <span className="font-medium">
587
+ {commonT('labels.notes')}:
588
+ </span>{' '}
589
+ {requestItem.approverNote}
590
+ </div>
591
+ ) : null}
592
+ {requestItem.reason ? (
593
+ <div className="text-xs text-muted-foreground">
594
+ <span className="font-medium">
595
+ {commonT('labels.reason')}:
596
+ </span>{' '}
597
+ {requestItem.reason}
598
+ </div>
599
+ ) : null}
600
+ </div>
509
601
  </div>
510
- </div>
511
- ))}
512
- </div>
602
+ ))}
603
+ </div>
604
+ ) : (
605
+ <div className="overflow-x-auto rounded-md border">
606
+ <Table>
607
+ <TableHeader>
608
+ <TableRow>
609
+ <TableHead>{commonT('labels.collaborator')}</TableHead>
610
+ <TableHead>{t('table.scope')}</TableHead>
611
+ <TableHead>{commonT('labels.timeline')}</TableHead>
612
+ <TableHead>{commonT('labels.approver')}</TableHead>
613
+ <TableHead>{commonT('labels.status')}</TableHead>
614
+ <TableHead className="text-right">
615
+ {commonT('labels.actions')}
616
+ </TableHead>
617
+ </TableRow>
618
+ </TableHeader>
619
+ <TableBody>
620
+ {requests.map((requestItem) => (
621
+ <TableRow key={requestItem.id} className="hover:bg-muted/30">
622
+ <TableCell className="font-medium">
623
+ {requestItem.collaboratorName}
624
+ </TableCell>
625
+ <TableCell>
626
+ <Badge variant="outline" className="text-xs">
627
+ {getRequestScopeLabel(requestItem.requestScope)}
628
+ </Badge>
629
+ </TableCell>
630
+ <TableCell>
631
+ {formatDateRange(
632
+ requestItem.effectiveStartDate,
633
+ requestItem.effectiveEndDate
634
+ )}
635
+ </TableCell>
636
+ <TableCell>
637
+ {requestItem.approverName ||
638
+ commonT('labels.notAssigned')}
639
+ </TableCell>
640
+ <TableCell>
641
+ <StatusBadge
642
+ label={getStatusLabel(requestItem.status)}
643
+ className={getStatusBadgeClass(requestItem.status)}
644
+ />
645
+ </TableCell>
646
+ <TableCell>
647
+ <div className="flex justify-end">
648
+ <Button
649
+ variant="outline"
650
+ size="sm"
651
+ className="cursor-pointer"
652
+ onClick={() => openDetails(requestItem)}
653
+ >
654
+ <Eye className="size-4" />
655
+ {t('actions.viewDetails')}
656
+ </Button>
657
+ </div>
658
+ </TableCell>
659
+ </TableRow>
660
+ ))}
661
+ </TableBody>
662
+ </Table>
663
+ </div>
664
+ )
513
665
  ) : (
514
666
  <EmptyState
515
667
  icon={<CalendarRange className="size-12" />}
@@ -528,6 +680,18 @@ export default function OperationsScheduleAdjustmentsPage() {
528
680
  />
529
681
  )}
530
682
 
683
+ <PaginationFooter
684
+ currentPage={page}
685
+ pageSize={pageSize}
686
+ totalItems={requestsResponse?.total ?? 0}
687
+ pageSizeOptions={[12, 24, 48]}
688
+ onPageChange={setPage}
689
+ onPageSizeChange={(size) => {
690
+ setPageSize(size);
691
+ setPage(1);
692
+ }}
693
+ />
694
+
531
695
  <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
532
696
  <SheetContent className="w-full overflow-y-auto sm:max-w-3xl">
533
697
  <SheetHeader>
@@ -722,6 +886,107 @@ export default function OperationsScheduleAdjustmentsPage() {
722
886
  />
723
887
  </SheetContent>
724
888
  </Sheet>
889
+
890
+ <Sheet open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
891
+ <SheetContent className="w-full overflow-y-auto sm:max-w-3xl">
892
+ <SheetHeader>
893
+ <SheetTitle>{t('details.title')}</SheetTitle>
894
+ <SheetDescription>{t('details.description')}</SheetDescription>
895
+ </SheetHeader>
896
+
897
+ {selectedRequest ? (
898
+ <div className="mt-6 space-y-5 px-4 pb-8">
899
+ <div className="rounded-lg border bg-muted/20 p-4">
900
+ <div className="flex flex-wrap items-start justify-between gap-3">
901
+ <div>
902
+ <div className="text-lg font-semibold">
903
+ {selectedRequest.collaboratorName}
904
+ </div>
905
+ <div className="mt-1 text-sm text-muted-foreground">
906
+ {getRequestScopeLabel(selectedRequest.requestScope)}
907
+ </div>
908
+ </div>
909
+ <StatusBadge
910
+ label={getStatusLabel(selectedRequest.status)}
911
+ className={getStatusBadgeClass(selectedRequest.status)}
912
+ />
913
+ </div>
914
+ </div>
915
+
916
+ <div className="grid gap-4 sm:grid-cols-2">
917
+ <div>
918
+ <div className="text-xs font-medium uppercase text-muted-foreground">
919
+ {commonT('labels.timeline')}
920
+ </div>
921
+ <div className="mt-1 text-sm">
922
+ {formatDateRange(
923
+ selectedRequest.effectiveStartDate,
924
+ selectedRequest.effectiveEndDate
925
+ )}
926
+ </div>
927
+ </div>
928
+ <div>
929
+ <div className="text-xs font-medium uppercase text-muted-foreground">
930
+ {commonT('labels.approver')}
931
+ </div>
932
+ <div className="mt-1 text-sm">
933
+ {selectedRequest.approverName ||
934
+ commonT('labels.notAssigned')}
935
+ </div>
936
+ </div>
937
+ </div>
938
+
939
+ <div className="grid gap-4 sm:grid-cols-2">
940
+ <div>
941
+ <div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
942
+ {t('table.currentSchedule')}
943
+ </div>
944
+ <div className="rounded-lg border p-3">
945
+ <SchedulePanel
946
+ days={selectedRequest.currentSchedule ?? []}
947
+ locale={currentLocaleCode}
948
+ emptyLabel={commonT('labels.notAssigned')}
949
+ />
950
+ </div>
951
+ </div>
952
+ <div>
953
+ <div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
954
+ {t('table.requestedSchedule')}
955
+ </div>
956
+ <div className="rounded-lg border p-3">
957
+ <SchedulePanel
958
+ days={
959
+ (selectedRequest.days ??
960
+ []) as OperationsScheduleAdjustmentDay[]
961
+ }
962
+ locale={currentLocaleCode}
963
+ emptyLabel={commonT('labels.notAssigned')}
964
+ />
965
+ </div>
966
+ </div>
967
+ </div>
968
+
969
+ <div>
970
+ <div className="text-xs font-medium uppercase text-muted-foreground">
971
+ {commonT('labels.reason')}
972
+ </div>
973
+ <div className="mt-1 rounded-lg border p-3 text-sm">
974
+ {selectedRequest.reason || commonT('labels.noNotes')}
975
+ </div>
976
+ </div>
977
+
978
+ <div>
979
+ <div className="text-xs font-medium uppercase text-muted-foreground">
980
+ {commonT('labels.notes')}
981
+ </div>
982
+ <div className="mt-1 rounded-lg border p-3 text-sm">
983
+ {selectedRequest.approverNote || commonT('labels.noNotes')}
984
+ </div>
985
+ </div>
986
+ </div>
987
+ ) : null}
988
+ </SheetContent>
989
+ </Sheet>
725
990
  </Page>
726
991
  );
727
992
  }