@hed-hog/operations 0.0.2

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 (49) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +18 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/operations.module.d.ts +3 -0
  6. package/dist/operations.module.d.ts.map +1 -0
  7. package/dist/operations.module.js +28 -0
  8. package/dist/operations.module.js.map +1 -0
  9. package/hedhog/data/menu.yaml +132 -0
  10. package/hedhog/data/role.yaml +7 -0
  11. package/hedhog/data/route.yaml +80 -0
  12. package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +56 -0
  13. package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +83 -0
  14. package/hedhog/frontend/app/_components/operations-header.tsx.ejs +29 -0
  15. package/hedhog/frontend/app/_components/section-card.tsx.ejs +32 -0
  16. package/hedhog/frontend/app/_components/status-badge.tsx.ejs +15 -0
  17. package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +142 -0
  18. package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +41 -0
  19. package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +74 -0
  20. package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +74 -0
  21. package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +60 -0
  22. package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +88 -0
  23. package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +84 -0
  24. package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +67 -0
  25. package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +10 -0
  26. package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +10 -0
  27. package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +10 -0
  28. package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +10 -0
  29. package/hedhog/frontend/app/_lib/types/operations.ts.ejs +95 -0
  30. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +25 -0
  31. package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +103 -0
  32. package/hedhog/frontend/app/_lib/utils/status.ts.ejs +80 -0
  33. package/hedhog/frontend/app/allocations/page.tsx.ejs +99 -0
  34. package/hedhog/frontend/app/approvals/page.tsx.ejs +147 -0
  35. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +108 -0
  36. package/hedhog/frontend/app/contracts/page.tsx.ejs +124 -0
  37. package/hedhog/frontend/app/layout.tsx.ejs +9 -0
  38. package/hedhog/frontend/app/page.tsx.ejs +177 -0
  39. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +186 -0
  40. package/hedhog/frontend/app/projects/page.tsx.ejs +111 -0
  41. package/hedhog/frontend/app/tasks/page.tsx.ejs +47 -0
  42. package/hedhog/frontend/app/timesheets/page.tsx.ejs +126 -0
  43. package/hedhog/frontend/messages/en.json +142 -0
  44. package/hedhog/frontend/messages/pt.json +142 -0
  45. package/package.json +37 -0
  46. package/src/index.ts +1 -0
  47. package/src/language/en.json +8 -0
  48. package/src/language/pt.json +8 -0
  49. package/src/operations.module.ts +15 -0
@@ -0,0 +1,103 @@
1
+ import { allocationsMock } from '../mocks/allocations.mock';
2
+ import { contractsMock } from '../mocks/contracts.mock';
3
+ import { projectsMock } from '../mocks/projects.mock';
4
+ import { tasksMock } from '../mocks/tasks.mock';
5
+ import { timesheetsMock } from '../mocks/timesheets.mock';
6
+ import { operationsUsersMock } from '../mocks/users.mock';
7
+ import { getApprovalLabel } from './status';
8
+
9
+ export function getDashboardMetrics() {
10
+ const totalActiveProjects = projectsMock.filter(
11
+ (project) => project.status === 'active'
12
+ ).length;
13
+ const hoursLoggedThisMonth = timesheetsMock.reduce(
14
+ (total, entry) => total + entry.hours,
15
+ 0
16
+ );
17
+ const hoursPendingApproval = timesheetsMock
18
+ .filter((entry) => entry.status === 'pending')
19
+ .reduce((total, entry) => total + entry.hours, 0);
20
+ const revenue = contractsMock
21
+ .filter((contract) => contract.status === 'active')
22
+ .reduce((total, contract) => total + contract.hourlyRate * 160, 0);
23
+ const utilization =
24
+ allocationsMock.reduce(
25
+ (total, allocation) => total + allocation.allocationPercent,
26
+ 0
27
+ ) / allocationsMock.length;
28
+
29
+ return {
30
+ totalActiveProjects,
31
+ hoursLoggedThisMonth,
32
+ hoursPendingApproval,
33
+ revenue,
34
+ utilization,
35
+ };
36
+ }
37
+
38
+ export function getHoursByProject() {
39
+ return projectsMock.map((project) => ({
40
+ name: project.name,
41
+ hours: timesheetsMock
42
+ .filter((entry) => entry.projectId === project.id)
43
+ .reduce((total, entry) => total + entry.hours, 0),
44
+ }));
45
+ }
46
+
47
+ export function getHoursByUser() {
48
+ return operationsUsersMock.map((user) => ({
49
+ name: user.name.split(' ')[0],
50
+ hours: timesheetsMock
51
+ .filter((entry) => entry.userId === user.id)
52
+ .reduce((total, entry) => total + entry.hours, 0),
53
+ }));
54
+ }
55
+
56
+ export function getApprovalStatusData() {
57
+ return ['approved', 'pending', 'rejected'].map((status) => ({
58
+ name: getApprovalLabel(status as 'approved' | 'pending' | 'rejected'),
59
+ value: timesheetsMock.filter((entry) => entry.status === status).length,
60
+ }));
61
+ }
62
+
63
+ export function getWeeklyTrend() {
64
+ return [
65
+ { week: 'W1', hours: 32 },
66
+ { week: 'W2', hours: 36 },
67
+ { week: 'W3', hours: 29 },
68
+ { week: 'W4', hours: 41 },
69
+ ];
70
+ }
71
+
72
+ export function getRecentTimesheetEntries() {
73
+ return [...timesheetsMock]
74
+ .sort((a, b) => b.date.localeCompare(a.date))
75
+ .slice(0, 6);
76
+ }
77
+
78
+ export function getDailyTotals() {
79
+ return timesheetsMock.reduce<Record<string, number>>((acc, entry) => {
80
+ acc[entry.date] = (acc[entry.date] || 0) + entry.hours;
81
+ return acc;
82
+ }, {});
83
+ }
84
+
85
+ export function getPendingApprovalsSummary() {
86
+ return {
87
+ pending: timesheetsMock.filter((entry) => entry.status === 'pending').length,
88
+ approved: timesheetsMock.filter((entry) => entry.status === 'approved').length,
89
+ rejected: timesheetsMock.filter((entry) => entry.status === 'rejected').length,
90
+ };
91
+ }
92
+
93
+ export function getProjectTeam(projectId: string) {
94
+ const project = projectsMock.find((item) => item.id === projectId);
95
+
96
+ return operationsUsersMock.filter((user) =>
97
+ project?.teamMemberIds.includes(user.id)
98
+ );
99
+ }
100
+
101
+ export function getProjectTasks(projectId: string) {
102
+ return tasksMock.filter((task) => task.projectId === projectId);
103
+ }
@@ -0,0 +1,80 @@
1
+ import type {
2
+ ApprovalStatus,
3
+ ContractStatus,
4
+ ContractType,
5
+ ProjectStatus,
6
+ TaskStatus,
7
+ } from '../types/operations';
8
+
9
+ export function getApprovalBadgeClasses(status: ApprovalStatus) {
10
+ return {
11
+ approved: 'bg-emerald-100 text-emerald-700',
12
+ pending: 'bg-amber-100 text-amber-700',
13
+ rejected: 'bg-rose-100 text-rose-700',
14
+ }[status];
15
+ }
16
+
17
+ export function getProjectBadgeClasses(status: ProjectStatus) {
18
+ return {
19
+ planning: 'bg-slate-100 text-slate-700',
20
+ active: 'bg-blue-100 text-blue-700',
21
+ 'at-risk': 'bg-orange-100 text-orange-700',
22
+ paused: 'bg-zinc-100 text-zinc-700',
23
+ completed: 'bg-emerald-100 text-emerald-700',
24
+ }[status];
25
+ }
26
+
27
+ export function getTaskBadgeClasses(status: TaskStatus) {
28
+ return {
29
+ backlog: 'bg-slate-100 text-slate-700',
30
+ todo: 'bg-cyan-100 text-cyan-700',
31
+ 'in-progress': 'bg-blue-100 text-blue-700',
32
+ review: 'bg-violet-100 text-violet-700',
33
+ done: 'bg-emerald-100 text-emerald-700',
34
+ }[status];
35
+ }
36
+
37
+ export function getContractBadgeClasses(status: ContractStatus) {
38
+ return {
39
+ active: 'bg-emerald-100 text-emerald-700',
40
+ draft: 'bg-slate-100 text-slate-700',
41
+ expired: 'bg-zinc-200 text-zinc-700',
42
+ renewal: 'bg-orange-100 text-orange-700',
43
+ }[status];
44
+ }
45
+
46
+ export function getContractTypeLabel(type: ContractType) {
47
+ return {
48
+ tm: 'T&M',
49
+ monthly: 'Monthly',
50
+ fixed: 'Fixed',
51
+ }[type];
52
+ }
53
+
54
+ export function getApprovalLabel(status: ApprovalStatus) {
55
+ return {
56
+ approved: 'Approved',
57
+ pending: 'Pending',
58
+ rejected: 'Rejected',
59
+ }[status];
60
+ }
61
+
62
+ export function getProjectStatusLabel(status: ProjectStatus) {
63
+ return {
64
+ planning: 'Planning',
65
+ active: 'Active',
66
+ 'at-risk': 'At Risk',
67
+ paused: 'Paused',
68
+ completed: 'Completed',
69
+ }[status];
70
+ }
71
+
72
+ export function getTaskStatusLabel(status: TaskStatus) {
73
+ return {
74
+ backlog: 'Backlog',
75
+ todo: 'Todo',
76
+ 'in-progress': 'In Progress',
77
+ review: 'Review',
78
+ done: 'Done',
79
+ }[status];
80
+ }
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import { Input } from '@/components/ui/input';
5
+ import {
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ } from '@/components/ui/table';
13
+ import { useTranslations } from 'next-intl';
14
+ import { useMemo, useState } from 'react';
15
+ import { AllocationCalendar } from '../_components/allocation-calendar';
16
+ import { OperationsHeader } from '../_components/operations-header';
17
+ import { SectionCard } from '../_components/section-card';
18
+ import { useOperationsData } from '../_lib/hooks/use-operations-data';
19
+ import { formatDate } from '../_lib/utils/format';
20
+
21
+ export default function AllocationsPage() {
22
+ const t = useTranslations('operations.AllocationsPage');
23
+ const { allocations, users, projects } = useOperationsData();
24
+ const [search, setSearch] = useState('');
25
+
26
+ const filteredAllocations = useMemo(
27
+ () =>
28
+ allocations.filter((allocation) => {
29
+ const user = users.find((item) => item.id === allocation.userId);
30
+ const project = projects.find((item) => item.id === allocation.projectId);
31
+ return `${user?.name} ${project?.name}`
32
+ .toLowerCase()
33
+ .includes(search.toLowerCase());
34
+ }),
35
+ [allocations, users, projects, search]
36
+ );
37
+
38
+ return (
39
+ <Page>
40
+ <OperationsHeader
41
+ title={t('title')}
42
+ description={t('description')}
43
+ current={t('breadcrumb')}
44
+ />
45
+
46
+ <SectionCard title={t('tableTitle')} description={t('tableDescription')}>
47
+ <div className="mb-4">
48
+ <Input
49
+ value={search}
50
+ onChange={(event) => setSearch(event.target.value)}
51
+ placeholder={t('searchPlaceholder')}
52
+ />
53
+ </div>
54
+ <Table>
55
+ <TableHeader>
56
+ <TableRow>
57
+ <TableHead>{t('columns.user')}</TableHead>
58
+ <TableHead>{t('columns.role')}</TableHead>
59
+ <TableHead>{t('columns.project')}</TableHead>
60
+ <TableHead>{t('columns.weeklyHours')}</TableHead>
61
+ <TableHead>{t('columns.allocation')}</TableHead>
62
+ <TableHead>{t('columns.startDate')}</TableHead>
63
+ <TableHead>{t('columns.endDate')}</TableHead>
64
+ </TableRow>
65
+ </TableHeader>
66
+ <TableBody>
67
+ {filteredAllocations.map((allocation) => {
68
+ const user = users.find((item) => item.id === allocation.userId);
69
+ const project = projects.find((item) => item.id === allocation.projectId);
70
+
71
+ return (
72
+ <TableRow key={allocation.id}>
73
+ <TableCell>{user?.name}</TableCell>
74
+ <TableCell>{allocation.role}</TableCell>
75
+ <TableCell>{project?.name}</TableCell>
76
+ <TableCell>{allocation.weeklyHours}h</TableCell>
77
+ <TableCell>{allocation.allocationPercent}%</TableCell>
78
+ <TableCell>{formatDate(allocation.startDate)}</TableCell>
79
+ <TableCell>{formatDate(allocation.endDate)}</TableCell>
80
+ </TableRow>
81
+ );
82
+ })}
83
+ </TableBody>
84
+ </Table>
85
+ </SectionCard>
86
+
87
+ <SectionCard
88
+ title={t('calendarTitle')}
89
+ description={t('calendarDescription')}
90
+ >
91
+ <AllocationCalendar
92
+ allocations={filteredAllocations}
93
+ users={users}
94
+ projects={projects}
95
+ />
96
+ </SectionCard>
97
+ </Page>
98
+ );
99
+ }
@@ -0,0 +1,147 @@
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Input } from '@/components/ui/input';
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from '@/components/ui/table';
14
+ import { useTranslations } from 'next-intl';
15
+ import { useMemo, useState } from 'react';
16
+ import { OperationsHeader } from '../_components/operations-header';
17
+ import { SectionCard } from '../_components/section-card';
18
+ import { StatusBadge } from '../_components/status-badge';
19
+ import { useOperationsData } from '../_lib/hooks/use-operations-data';
20
+ import { formatDate, formatHours } from '../_lib/utils/format';
21
+ import { getApprovalBadgeClasses, getApprovalLabel } from '../_lib/utils/status';
22
+
23
+ export default function ApprovalsPage() {
24
+ const t = useTranslations('operations.ApprovalsPage');
25
+ const { timesheets, users, projects, approvalSummary } = useOperationsData();
26
+ const [search, setSearch] = useState('');
27
+ const [statuses, setStatuses] = useState<
28
+ Record<string, 'approved' | 'pending' | 'rejected'>
29
+ >(() =>
30
+ timesheets.reduce(
31
+ (acc, entry) => {
32
+ acc[entry.id] = entry.status;
33
+ return acc;
34
+ },
35
+ {} as Record<string, 'approved' | 'pending' | 'rejected'>
36
+ )
37
+ );
38
+
39
+ const approvalQueue = useMemo(
40
+ () =>
41
+ timesheets.filter((entry) => {
42
+ const user = users.find((item) => item.id === entry.userId);
43
+ const project = projects.find((item) => item.id === entry.projectId);
44
+
45
+ return `${user?.name} ${project?.name} ${entry.description}`
46
+ .toLowerCase()
47
+ .includes(search.toLowerCase());
48
+ }),
49
+ [timesheets, users, projects, search]
50
+ );
51
+
52
+ return (
53
+ <Page>
54
+ <OperationsHeader
55
+ title={t('title')}
56
+ description={t('description')}
57
+ current={t('breadcrumb')}
58
+ />
59
+
60
+ <div className="grid gap-4 md:grid-cols-3">
61
+ <SectionCard title={t('cards.pending')}>
62
+ <p className="text-3xl font-semibold">{approvalSummary.pending}</p>
63
+ </SectionCard>
64
+ <SectionCard title={t('cards.approved')}>
65
+ <p className="text-3xl font-semibold">{approvalSummary.approved}</p>
66
+ </SectionCard>
67
+ <SectionCard title={t('cards.rejected')}>
68
+ <p className="text-3xl font-semibold">{approvalSummary.rejected}</p>
69
+ </SectionCard>
70
+ </div>
71
+
72
+ <SectionCard title={t('queueTitle')} description={t('queueDescription')}>
73
+ <div className="mb-4">
74
+ <Input
75
+ value={search}
76
+ onChange={(event) => setSearch(event.target.value)}
77
+ placeholder={t('searchPlaceholder')}
78
+ />
79
+ </div>
80
+
81
+ <Table>
82
+ <TableHeader>
83
+ <TableRow>
84
+ <TableHead>{t('columns.user')}</TableHead>
85
+ <TableHead>{t('columns.project')}</TableHead>
86
+ <TableHead>{t('columns.date')}</TableHead>
87
+ <TableHead>{t('columns.hours')}</TableHead>
88
+ <TableHead>{t('columns.description')}</TableHead>
89
+ <TableHead>{t('columns.status')}</TableHead>
90
+ <TableHead>{t('columns.actions')}</TableHead>
91
+ </TableRow>
92
+ </TableHeader>
93
+ <TableBody>
94
+ {approvalQueue.map((entry) => {
95
+ const user = users.find((item) => item.id === entry.userId);
96
+ const project = projects.find((item) => item.id === entry.projectId);
97
+ const currentStatus = statuses[entry.id] ?? 'pending';
98
+
99
+ return (
100
+ <TableRow key={entry.id}>
101
+ <TableCell>{user?.name}</TableCell>
102
+ <TableCell>{project?.name}</TableCell>
103
+ <TableCell>{formatDate(entry.date)}</TableCell>
104
+ <TableCell>{formatHours(entry.hours)}</TableCell>
105
+ <TableCell>{entry.description}</TableCell>
106
+ <TableCell>
107
+ <StatusBadge
108
+ label={getApprovalLabel(currentStatus)}
109
+ className={getApprovalBadgeClasses(currentStatus)}
110
+ />
111
+ </TableCell>
112
+ <TableCell>
113
+ <div className="flex gap-2">
114
+ <Button
115
+ size="sm"
116
+ onClick={() =>
117
+ setStatuses((current) => ({
118
+ ...current,
119
+ [entry.id]: 'approved',
120
+ }))
121
+ }
122
+ >
123
+ Approve
124
+ </Button>
125
+ <Button
126
+ size="sm"
127
+ variant="outline"
128
+ onClick={() =>
129
+ setStatuses((current) => ({
130
+ ...current,
131
+ [entry.id]: 'rejected',
132
+ }))
133
+ }
134
+ >
135
+ Reject
136
+ </Button>
137
+ </div>
138
+ </TableCell>
139
+ </TableRow>
140
+ );
141
+ })}
142
+ </TableBody>
143
+ </Table>
144
+ </SectionCard>
145
+ </Page>
146
+ );
147
+ }
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import { useParams } from 'next/navigation';
5
+ import { OperationsHeader } from '../../_components/operations-header';
6
+ import { SectionCard } from '../../_components/section-card';
7
+ import { StatusBadge } from '../../_components/status-badge';
8
+ import { useOperationsData } from '../../_lib/hooks/use-operations-data';
9
+ import { formatCurrency, formatDate } from '../../_lib/utils/format';
10
+ import {
11
+ getContractBadgeClasses,
12
+ getContractTypeLabel,
13
+ } from '../../_lib/utils/status';
14
+
15
+ export default function ContractDetailsPage() {
16
+ const params = useParams<{ id: string }>();
17
+ const { contracts, projects } = useOperationsData();
18
+ const contract = contracts.find((item) => item.id === params.id);
19
+
20
+ if (!contract) {
21
+ return <Page>Contract not found.</Page>;
22
+ }
23
+
24
+ const linkedProjects = projects.filter((project) =>
25
+ contract.linkedProjectIds.includes(project.id)
26
+ );
27
+
28
+ return (
29
+ <Page>
30
+ <OperationsHeader
31
+ title={contract.name}
32
+ description="Static mock detail page with production-ready structure."
33
+ current="Contract Details"
34
+ />
35
+
36
+ <div className="grid gap-4 xl:grid-cols-2">
37
+ <SectionCard title="Contract Info">
38
+ <div className="grid gap-3 text-sm sm:grid-cols-2">
39
+ <p>
40
+ <span className="font-medium">Client:</span> {contract.client}
41
+ </p>
42
+ <p>
43
+ <span className="font-medium">Type:</span>{' '}
44
+ {getContractTypeLabel(contract.type)}
45
+ </p>
46
+ <p>
47
+ <span className="font-medium">Start:</span>{' '}
48
+ {formatDate(contract.startDate)}
49
+ </p>
50
+ <p>
51
+ <span className="font-medium">End:</span>{' '}
52
+ {formatDate(contract.endDate)}
53
+ </p>
54
+ <p>
55
+ <span className="font-medium">Hour Rate:</span>{' '}
56
+ {formatCurrency(contract.hourlyRate)}
57
+ </p>
58
+ <p>
59
+ <span className="font-medium">Hour Limit:</span>{' '}
60
+ {contract.hourLimit}h
61
+ </p>
62
+ <div className="sm:col-span-2">
63
+ <StatusBadge
64
+ label={contract.status}
65
+ className={getContractBadgeClasses(contract.status)}
66
+ />
67
+ </div>
68
+ </div>
69
+ </SectionCard>
70
+
71
+ <SectionCard title="Linked Projects">
72
+ <div className="space-y-3">
73
+ {linkedProjects.map((project) => (
74
+ <div key={project.id} className="rounded-lg border p-3">
75
+ <p className="font-medium">{project.name}</p>
76
+ <p className="text-sm text-muted-foreground">{project.client}</p>
77
+ </div>
78
+ ))}
79
+ </div>
80
+ </SectionCard>
81
+
82
+ <SectionCard title="Billing Rules">
83
+ <ul className="space-y-2 text-sm">
84
+ {contract.billingRules.map((rule) => (
85
+ <li key={rule} className="rounded-md bg-muted/40 p-3">
86
+ {rule}
87
+ </li>
88
+ ))}
89
+ </ul>
90
+ </SectionCard>
91
+
92
+ <SectionCard title="SLA">
93
+ <p className="text-sm text-muted-foreground">{contract.sla}</p>
94
+ </SectionCard>
95
+
96
+ <SectionCard title="Adjustments / Revisions" className="xl:col-span-2">
97
+ <ul className="space-y-2 text-sm">
98
+ {contract.revisions.map((revision) => (
99
+ <li key={revision} className="rounded-md border p-3">
100
+ {revision}
101
+ </li>
102
+ ))}
103
+ </ul>
104
+ </SectionCard>
105
+ </div>
106
+ </Page>
107
+ );
108
+ }
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import { Input } from '@/components/ui/input';
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from '@/components/ui/select';
12
+ import {
13
+ Table,
14
+ TableBody,
15
+ TableCell,
16
+ TableHead,
17
+ TableHeader,
18
+ TableRow,
19
+ } from '@/components/ui/table';
20
+ import { useTranslations } from 'next-intl';
21
+ import Link from 'next/link';
22
+ import { useMemo, useState } from 'react';
23
+ import { OperationsHeader } from '../_components/operations-header';
24
+ import { SectionCard } from '../_components/section-card';
25
+ import { StatusBadge } from '../_components/status-badge';
26
+ import { useOperationsData } from '../_lib/hooks/use-operations-data';
27
+ import { formatCurrency, formatDate } from '../_lib/utils/format';
28
+ import {
29
+ getContractBadgeClasses,
30
+ getContractTypeLabel,
31
+ } from '../_lib/utils/status';
32
+
33
+ export default function ContractsPage() {
34
+ const t = useTranslations('operations.ContractsPage');
35
+ const { contracts } = useOperationsData();
36
+ const [search, setSearch] = useState('');
37
+ const [status, setStatus] = useState('all');
38
+
39
+ const filteredContracts = useMemo(
40
+ () =>
41
+ contracts.filter((contract) => {
42
+ const matchesSearch =
43
+ `${contract.name} ${contract.client}`
44
+ .toLowerCase()
45
+ .includes(search.toLowerCase());
46
+ const matchesStatus = status === 'all' || contract.status === status;
47
+ return matchesSearch && matchesStatus;
48
+ }),
49
+ [contracts, search, status]
50
+ );
51
+
52
+ return (
53
+ <Page>
54
+ <OperationsHeader
55
+ title={t('title')}
56
+ description={t('description')}
57
+ current={t('breadcrumb')}
58
+ />
59
+
60
+ <SectionCard title={t('tableTitle')} description={t('tableDescription')}>
61
+ <div className="mb-4 flex flex-col gap-3 lg:flex-row">
62
+ <Input
63
+ value={search}
64
+ onChange={(event) => setSearch(event.target.value)}
65
+ placeholder={t('searchPlaceholder')}
66
+ />
67
+ <Select value={status} onValueChange={setStatus}>
68
+ <SelectTrigger className="w-full lg:w-[220px]">
69
+ <SelectValue />
70
+ </SelectTrigger>
71
+ <SelectContent>
72
+ <SelectItem value="all">{t('filters.all')}</SelectItem>
73
+ <SelectItem value="active">{t('filters.active')}</SelectItem>
74
+ <SelectItem value="renewal">{t('filters.renewal')}</SelectItem>
75
+ <SelectItem value="expired">{t('filters.expired')}</SelectItem>
76
+ <SelectItem value="draft">{t('filters.draft')}</SelectItem>
77
+ </SelectContent>
78
+ </Select>
79
+ </div>
80
+
81
+ <Table>
82
+ <TableHeader>
83
+ <TableRow>
84
+ <TableHead>{t('columns.name')}</TableHead>
85
+ <TableHead>{t('columns.client')}</TableHead>
86
+ <TableHead>{t('columns.type')}</TableHead>
87
+ <TableHead>{t('columns.startDate')}</TableHead>
88
+ <TableHead>{t('columns.endDate')}</TableHead>
89
+ <TableHead>{t('columns.hourRate')}</TableHead>
90
+ <TableHead>{t('columns.hourLimit')}</TableHead>
91
+ <TableHead>{t('columns.status')}</TableHead>
92
+ </TableRow>
93
+ </TableHeader>
94
+ <TableBody>
95
+ {filteredContracts.map((contract) => (
96
+ <TableRow key={contract.id}>
97
+ <TableCell>
98
+ <Link
99
+ className="font-medium text-primary hover:underline"
100
+ href={`/operations/contracts/${contract.id}`}
101
+ >
102
+ {contract.name}
103
+ </Link>
104
+ </TableCell>
105
+ <TableCell>{contract.client}</TableCell>
106
+ <TableCell>{getContractTypeLabel(contract.type)}</TableCell>
107
+ <TableCell>{formatDate(contract.startDate)}</TableCell>
108
+ <TableCell>{formatDate(contract.endDate)}</TableCell>
109
+ <TableCell>{formatCurrency(contract.hourlyRate)}</TableCell>
110
+ <TableCell>{contract.hourLimit}h</TableCell>
111
+ <TableCell>
112
+ <StatusBadge
113
+ label={contract.status}
114
+ className={getContractBadgeClasses(contract.status)}
115
+ />
116
+ </TableCell>
117
+ </TableRow>
118
+ ))}
119
+ </TableBody>
120
+ </Table>
121
+ </SectionCard>
122
+ </Page>
123
+ );
124
+ }
@@ -0,0 +1,9 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ export default function OperationsLayout({
4
+ children,
5
+ }: {
6
+ children: ReactNode;
7
+ }) {
8
+ return children;
9
+ }