@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,177 @@
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import { KpiCard } from '@/components/ui/kpi-card';
5
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
6
+ import { Activity, BadgeDollarSign, Clock3, FolderKanban, Users } from 'lucide-react';
7
+ import { useTranslations } from 'next-intl';
8
+ import {
9
+ Bar,
10
+ BarChart,
11
+ CartesianGrid,
12
+ Cell,
13
+ Line,
14
+ LineChart,
15
+ Pie,
16
+ PieChart,
17
+ ResponsiveContainer,
18
+ Tooltip,
19
+ XAxis,
20
+ YAxis,
21
+ } from 'recharts';
22
+ import { OperationsHeader } from './_components/operations-header';
23
+ import { SectionCard } from './_components/section-card';
24
+ import { StatusBadge } from './_components/status-badge';
25
+ import { useOperationsData } from './_lib/hooks/use-operations-data';
26
+ import { formatCurrency, formatHours } from './_lib/utils/format';
27
+ import { getApprovalBadgeClasses, getApprovalLabel } from './_lib/utils/status';
28
+
29
+ const chartColors = ['#0f766e', '#1d4ed8', '#f97316', '#be123c', '#7c3aed'];
30
+
31
+ export default function OperationsDashboardPage() {
32
+ const t = useTranslations('operations.DashboardPage');
33
+ const {
34
+ dashboardMetrics,
35
+ hoursByProject,
36
+ hoursByUser,
37
+ approvalStatusData,
38
+ weeklyTrendData,
39
+ recentTimesheets,
40
+ users,
41
+ projects,
42
+ tasks,
43
+ } = useOperationsData();
44
+
45
+ return (
46
+ <Page>
47
+ <OperationsHeader
48
+ title={t('title')}
49
+ description={t('description')}
50
+ current={t('breadcrumb')}
51
+ />
52
+
53
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
54
+ <KpiCard
55
+ title={t('cards.activeProjects')}
56
+ value={dashboardMetrics.totalActiveProjects}
57
+ icon={FolderKanban}
58
+ />
59
+ <KpiCard
60
+ title={t('cards.loggedHours')}
61
+ value={formatHours(dashboardMetrics.hoursLoggedThisMonth)}
62
+ icon={Clock3}
63
+ />
64
+ <KpiCard
65
+ title={t('cards.pendingHours')}
66
+ value={formatHours(dashboardMetrics.hoursPendingApproval)}
67
+ icon={Activity}
68
+ />
69
+ <KpiCard
70
+ title={t('cards.revenue')}
71
+ value={formatCurrency(dashboardMetrics.revenue)}
72
+ icon={BadgeDollarSign}
73
+ />
74
+ <KpiCard
75
+ title={t('cards.utilization')}
76
+ value={`${dashboardMetrics.utilization.toFixed(0)}%`}
77
+ icon={Users}
78
+ />
79
+ </div>
80
+
81
+ <div className="grid gap-4 xl:grid-cols-2">
82
+ <SectionCard title={t('charts.hoursByProject')} description={t('charts.hoursByProjectDescription')}>
83
+ <ResponsiveContainer width="100%" height={280}>
84
+ <BarChart data={hoursByProject}>
85
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
86
+ <XAxis dataKey="name" tick={{ fontSize: 11 }} interval={0} angle={-15} textAnchor="end" height={55} />
87
+ <YAxis />
88
+ <Tooltip />
89
+ <Bar dataKey="hours" radius={[6, 6, 0, 0]}>
90
+ {hoursByProject.map((item, index) => (
91
+ <Cell key={item.name} fill={chartColors[index % chartColors.length]} />
92
+ ))}
93
+ </Bar>
94
+ </BarChart>
95
+ </ResponsiveContainer>
96
+ </SectionCard>
97
+
98
+ <SectionCard title={t('charts.hoursByUser')} description={t('charts.hoursByUserDescription')}>
99
+ <ResponsiveContainer width="100%" height={280}>
100
+ <BarChart data={hoursByUser}>
101
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
102
+ <XAxis dataKey="name" />
103
+ <YAxis />
104
+ <Tooltip />
105
+ <Bar dataKey="hours" radius={[6, 6, 0, 0]}>
106
+ {hoursByUser.map((item, index) => (
107
+ <Cell key={item.name} fill={chartColors[index % chartColors.length]} />
108
+ ))}
109
+ </Bar>
110
+ </BarChart>
111
+ </ResponsiveContainer>
112
+ </SectionCard>
113
+
114
+ <SectionCard title={t('charts.approvalStatus')} description={t('charts.approvalStatusDescription')}>
115
+ <ResponsiveContainer width="100%" height={280}>
116
+ <PieChart>
117
+ <Pie data={approvalStatusData} dataKey="value" nameKey="name" outerRadius={110}>
118
+ {approvalStatusData.map((item, index) => (
119
+ <Cell key={item.name} fill={chartColors[index % chartColors.length]} />
120
+ ))}
121
+ </Pie>
122
+ <Tooltip />
123
+ </PieChart>
124
+ </ResponsiveContainer>
125
+ </SectionCard>
126
+
127
+ <SectionCard title={t('charts.weeklyTrend')} description={t('charts.weeklyTrendDescription')}>
128
+ <ResponsiveContainer width="100%" height={280}>
129
+ <LineChart data={weeklyTrendData}>
130
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
131
+ <XAxis dataKey="week" />
132
+ <YAxis />
133
+ <Tooltip />
134
+ <Line dataKey="hours" type="monotone" stroke="#1d4ed8" strokeWidth={3} />
135
+ </LineChart>
136
+ </ResponsiveContainer>
137
+ </SectionCard>
138
+ </div>
139
+
140
+ <SectionCard title={t('recentTable.title')} description={t('recentTable.description')}>
141
+ <Table>
142
+ <TableHeader>
143
+ <TableRow>
144
+ <TableHead>{t('recentTable.user')}</TableHead>
145
+ <TableHead>{t('recentTable.project')}</TableHead>
146
+ <TableHead>{t('recentTable.task')}</TableHead>
147
+ <TableHead>{t('recentTable.hours')}</TableHead>
148
+ <TableHead>{t('recentTable.status')}</TableHead>
149
+ </TableRow>
150
+ </TableHeader>
151
+ <TableBody>
152
+ {recentTimesheets.map((entry) => {
153
+ const user = users.find((item) => item.id === entry.userId);
154
+ const project = projects.find((item) => item.id === entry.projectId);
155
+ const task = tasks.find((item) => item.id === entry.taskId);
156
+
157
+ return (
158
+ <TableRow key={entry.id}>
159
+ <TableCell>{user?.name}</TableCell>
160
+ <TableCell>{project?.name}</TableCell>
161
+ <TableCell>{task?.title}</TableCell>
162
+ <TableCell>{formatHours(entry.hours)}</TableCell>
163
+ <TableCell>
164
+ <StatusBadge
165
+ label={getApprovalLabel(entry.status)}
166
+ className={getApprovalBadgeClasses(entry.status)}
167
+ />
168
+ </TableCell>
169
+ </TableRow>
170
+ );
171
+ })}
172
+ </TableBody>
173
+ </Table>
174
+ </SectionCard>
175
+ </Page>
176
+ );
177
+ }
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from '@/components/ui/table';
12
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
13
+ import { useParams } from 'next/navigation';
14
+ import { KanbanBoard } from '../../_components/kanban-board';
15
+ import { OperationsHeader } from '../../_components/operations-header';
16
+ import { SectionCard } from '../../_components/section-card';
17
+ import { StatusBadge } from '../../_components/status-badge';
18
+ import { useOperationsData } from '../../_lib/hooks/use-operations-data';
19
+ import {
20
+ formatCurrency,
21
+ formatDate,
22
+ formatHours,
23
+ } from '../../_lib/utils/format';
24
+ import { getProjectTeam } from '../../_lib/utils/metrics';
25
+ import {
26
+ getApprovalBadgeClasses,
27
+ getApprovalLabel,
28
+ getProjectBadgeClasses,
29
+ getProjectStatusLabel,
30
+ } from '../../_lib/utils/status';
31
+
32
+ export default function ProjectDetailsPage() {
33
+ const params = useParams<{ id: string }>();
34
+ const { projects, contracts, users, tasks, timesheets } = useOperationsData();
35
+ const project = projects.find((item) => item.id === params.id);
36
+
37
+ if (!project) {
38
+ return <Page>Project not found.</Page>;
39
+ }
40
+
41
+ const contract = contracts.find((item) => item.id === project.contractId);
42
+ const team = getProjectTeam(project.id);
43
+ const projectTasks = tasks.filter((task) => task.projectId === project.id);
44
+ const projectTimesheets = timesheets.filter(
45
+ (entry) => entry.projectId === project.id
46
+ );
47
+
48
+ return (
49
+ <Page>
50
+ <OperationsHeader
51
+ title={project.name}
52
+ description={project.description}
53
+ current="Project Details"
54
+ />
55
+
56
+ <Tabs defaultValue="overview" className="space-y-4">
57
+ <TabsList>
58
+ <TabsTrigger value="overview">Overview</TabsTrigger>
59
+ <TabsTrigger value="team">Team</TabsTrigger>
60
+ <TabsTrigger value="tasks">Tasks</TabsTrigger>
61
+ <TabsTrigger value="timesheets">Timesheets</TabsTrigger>
62
+ <TabsTrigger value="costs">Costs</TabsTrigger>
63
+ </TabsList>
64
+
65
+ <TabsContent value="overview">
66
+ <SectionCard title="Project Overview">
67
+ <div className="grid gap-4 text-sm md:grid-cols-2">
68
+ <p>
69
+ <span className="font-medium">Status:</span>{' '}
70
+ <StatusBadge
71
+ label={getProjectStatusLabel(project.status)}
72
+ className={getProjectBadgeClasses(project.status)}
73
+ />
74
+ </p>
75
+ <p>
76
+ <span className="font-medium">Progress:</span> {project.progress}%
77
+ </p>
78
+ <p>
79
+ <span className="font-medium">Client:</span> {project.client}
80
+ </p>
81
+ <p>
82
+ <span className="font-medium">Contract:</span> {contract?.name}
83
+ </p>
84
+ <p className="md:col-span-2">
85
+ <span className="font-medium">Description:</span>{' '}
86
+ {project.description}
87
+ </p>
88
+ </div>
89
+ </SectionCard>
90
+ </TabsContent>
91
+
92
+ <TabsContent value="team">
93
+ <SectionCard title="Team Allocation">
94
+ <Table>
95
+ <TableHeader>
96
+ <TableRow>
97
+ <TableHead>Member</TableHead>
98
+ <TableHead>Role</TableHead>
99
+ <TableHead>Hour Rate</TableHead>
100
+ <TableHead>Allocation %</TableHead>
101
+ </TableRow>
102
+ </TableHeader>
103
+ <TableBody>
104
+ {team.map((member) => (
105
+ <TableRow key={member.id}>
106
+ <TableCell>{member.name}</TableCell>
107
+ <TableCell>{member.role}</TableCell>
108
+ <TableCell>{formatCurrency(member.hourlyRate)}</TableCell>
109
+ <TableCell>{member.utilizationTarget}%</TableCell>
110
+ </TableRow>
111
+ ))}
112
+ </TableBody>
113
+ </Table>
114
+ </SectionCard>
115
+ </TabsContent>
116
+
117
+ <TabsContent value="tasks">
118
+ <SectionCard title="Kanban Preview">
119
+ <KanbanBoard tasks={projectTasks} users={users} />
120
+ </SectionCard>
121
+ </TabsContent>
122
+
123
+ <TabsContent value="timesheets">
124
+ <SectionCard title="Project Timesheets">
125
+ <Table>
126
+ <TableHeader>
127
+ <TableRow>
128
+ <TableHead>Date</TableHead>
129
+ <TableHead>User</TableHead>
130
+ <TableHead>Hours</TableHead>
131
+ <TableHead>Description</TableHead>
132
+ <TableHead>Status</TableHead>
133
+ </TableRow>
134
+ </TableHeader>
135
+ <TableBody>
136
+ {projectTimesheets.map((entry) => {
137
+ const user = users.find((item) => item.id === entry.userId);
138
+
139
+ return (
140
+ <TableRow key={entry.id}>
141
+ <TableCell>{formatDate(entry.date)}</TableCell>
142
+ <TableCell>{user?.name}</TableCell>
143
+ <TableCell>{formatHours(entry.hours)}</TableCell>
144
+ <TableCell>{entry.description}</TableCell>
145
+ <TableCell>
146
+ <StatusBadge
147
+ label={getApprovalLabel(entry.status)}
148
+ className={getApprovalBadgeClasses(entry.status)}
149
+ />
150
+ </TableCell>
151
+ </TableRow>
152
+ );
153
+ })}
154
+ </TableBody>
155
+ </Table>
156
+ </SectionCard>
157
+ </TabsContent>
158
+
159
+ <TabsContent value="costs">
160
+ <SectionCard title="Costs (Mock)">
161
+ <div className="grid gap-3 md:grid-cols-3">
162
+ <div className="rounded-lg border p-4">
163
+ <p className="text-sm text-muted-foreground">Budget</p>
164
+ <p className="text-xl font-semibold">
165
+ {formatCurrency(project.budget)}
166
+ </p>
167
+ </div>
168
+ <div className="rounded-lg border p-4">
169
+ <p className="text-sm text-muted-foreground">Logged Hours</p>
170
+ <p className="text-xl font-semibold">
171
+ {formatHours(project.hoursLogged)}
172
+ </p>
173
+ </div>
174
+ <div className="rounded-lg border p-4">
175
+ <p className="text-sm text-muted-foreground">Projected End</p>
176
+ <p className="text-xl font-semibold">
177
+ {formatDate(project.endDate)}
178
+ </p>
179
+ </div>
180
+ </div>
181
+ </SectionCard>
182
+ </TabsContent>
183
+ </Tabs>
184
+ </Page>
185
+ );
186
+ }
@@ -0,0 +1,111 @@
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import { Input } from '@/components/ui/input';
5
+ import { Progress } from '@/components/ui/progress';
6
+ import { useTranslations } from 'next-intl';
7
+ import Link from 'next/link';
8
+ import { useMemo, useState } from 'react';
9
+ import { OperationsHeader } from '../_components/operations-header';
10
+ import { SectionCard } from '../_components/section-card';
11
+ import { StatusBadge } from '../_components/status-badge';
12
+ import { useOperationsData } from '../_lib/hooks/use-operations-data';
13
+ import { formatDate, formatHours } from '../_lib/utils/format';
14
+ import {
15
+ getProjectBadgeClasses,
16
+ getProjectStatusLabel,
17
+ } from '../_lib/utils/status';
18
+
19
+ export default function ProjectsPage() {
20
+ const t = useTranslations('operations.ProjectsPage');
21
+ const { projects, users } = useOperationsData();
22
+ const [search, setSearch] = useState('');
23
+
24
+ const filteredProjects = useMemo(
25
+ () =>
26
+ projects.filter((project) =>
27
+ `${project.name} ${project.client}`
28
+ .toLowerCase()
29
+ .includes(search.toLowerCase())
30
+ ),
31
+ [projects, search]
32
+ );
33
+
34
+ return (
35
+ <Page>
36
+ <OperationsHeader
37
+ title={t('title')}
38
+ description={t('description')}
39
+ current={t('breadcrumb')}
40
+ />
41
+
42
+ <SectionCard title={t('gridTitle')} description={t('gridDescription')}>
43
+ <div className="mb-4">
44
+ <Input
45
+ value={search}
46
+ onChange={(event) => setSearch(event.target.value)}
47
+ placeholder={t('searchPlaceholder')}
48
+ />
49
+ </div>
50
+ <div className="grid gap-4 xl:grid-cols-2">
51
+ {filteredProjects.map((project) => {
52
+ const memberNames = users
53
+ .filter((user) => project.teamMemberIds.includes(user.id))
54
+ .map((user) => user.name)
55
+ .join(', ');
56
+
57
+ return (
58
+ <Link
59
+ key={project.id}
60
+ href={`/operations/projects/${project.id}`}
61
+ className="rounded-xl border p-5 transition hover:border-primary/40 hover:shadow-md"
62
+ >
63
+ <div className="space-y-4">
64
+ <div className="flex items-start justify-between gap-4">
65
+ <div>
66
+ <p className="text-lg font-semibold">{project.name}</p>
67
+ <p className="text-sm text-muted-foreground">
68
+ {project.client}
69
+ </p>
70
+ </div>
71
+ <StatusBadge
72
+ label={getProjectStatusLabel(project.status)}
73
+ className={getProjectBadgeClasses(project.status)}
74
+ />
75
+ </div>
76
+
77
+ <div>
78
+ <div className="mb-2 flex items-center justify-between text-sm">
79
+ <span>{t('progress')}</span>
80
+ <span>{project.progress}%</span>
81
+ </div>
82
+ <Progress value={project.progress} />
83
+ </div>
84
+
85
+ <div className="grid gap-3 text-sm sm:grid-cols-2">
86
+ <p>
87
+ <span className="font-medium">{t('teamMembers')}:</span>{' '}
88
+ {memberNames}
89
+ </p>
90
+ <p>
91
+ <span className="font-medium">{t('hoursLogged')}:</span>{' '}
92
+ {formatHours(project.hoursLogged)}
93
+ </p>
94
+ <p>
95
+ <span className="font-medium">{t('startDate')}:</span>{' '}
96
+ {formatDate(project.startDate)}
97
+ </p>
98
+ <p>
99
+ <span className="font-medium">{t('endDate')}:</span>{' '}
100
+ {formatDate(project.endDate)}
101
+ </p>
102
+ </div>
103
+ </div>
104
+ </Link>
105
+ );
106
+ })}
107
+ </div>
108
+ </SectionCard>
109
+ </Page>
110
+ );
111
+ }
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import { Input } from '@/components/ui/input';
5
+ import { useTranslations } from 'next-intl';
6
+ import { useMemo, useState } from 'react';
7
+ import { KanbanBoard } from '../_components/kanban-board';
8
+ import { OperationsHeader } from '../_components/operations-header';
9
+ import { SectionCard } from '../_components/section-card';
10
+ import { useOperationsData } from '../_lib/hooks/use-operations-data';
11
+
12
+ export default function TasksPage() {
13
+ const t = useTranslations('operations.TasksPage');
14
+ const { tasks, users } = useOperationsData();
15
+ const [search, setSearch] = useState('');
16
+
17
+ const filteredTasks = useMemo(
18
+ () =>
19
+ tasks.filter((task) =>
20
+ `${task.title} ${task.projectName} ${task.labels.join(' ')}`
21
+ .toLowerCase()
22
+ .includes(search.toLowerCase())
23
+ ),
24
+ [tasks, search]
25
+ );
26
+
27
+ return (
28
+ <Page>
29
+ <OperationsHeader
30
+ title={t('title')}
31
+ description={t('description')}
32
+ current={t('breadcrumb')}
33
+ />
34
+
35
+ <SectionCard title={t('boardTitle')} description={t('boardDescription')}>
36
+ <div className="mb-4">
37
+ <Input
38
+ value={search}
39
+ onChange={(event) => setSearch(event.target.value)}
40
+ placeholder={t('searchPlaceholder')}
41
+ />
42
+ </div>
43
+ <KanbanBoard tasks={filteredTasks} users={users} />
44
+ </SectionCard>
45
+ </Page>
46
+ );
47
+ }
@@ -0,0 +1,126 @@
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 { OperationsHeader } from '../_components/operations-header';
16
+ import { SectionCard } from '../_components/section-card';
17
+ import { StatusBadge } from '../_components/status-badge';
18
+ import { TimesheetEntryDialog } from '../_components/timesheet-entry-dialog';
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 TimesheetsPage() {
24
+ const t = useTranslations('operations.TimesheetsPage');
25
+ const { timesheets, users, projects, tasks, dailyTotals } =
26
+ useOperationsData();
27
+ const [search, setSearch] = useState('');
28
+
29
+ const filteredTimesheets = useMemo(
30
+ () =>
31
+ timesheets.filter((entry) => {
32
+ const user = users.find((item) => item.id === entry.userId);
33
+ const project = projects.find((item) => item.id === entry.projectId);
34
+ const task = tasks.find((item) => item.id === entry.taskId);
35
+
36
+ return `${user?.name} ${project?.name} ${task?.title} ${entry.description}`
37
+ .toLowerCase()
38
+ .includes(search.toLowerCase());
39
+ }),
40
+ [timesheets, users, projects, tasks, search]
41
+ );
42
+
43
+ const weeklyTotal = filteredTimesheets.reduce(
44
+ (sum, entry) => sum + entry.hours,
45
+ 0
46
+ );
47
+
48
+ return (
49
+ <Page>
50
+ <OperationsHeader
51
+ title={t('title')}
52
+ description={t('description')}
53
+ current={t('breadcrumb')}
54
+ actions={
55
+ <TimesheetEntryDialog users={users} projects={projects} tasks={tasks} />
56
+ }
57
+ />
58
+
59
+ <div className="grid gap-4 md:grid-cols-3">
60
+ <SectionCard title={t('summary.totalHours')}>
61
+ <p className="text-3xl font-semibold">{formatHours(weeklyTotal)}</p>
62
+ </SectionCard>
63
+ <SectionCard title={t('summary.dailyTotals')}>
64
+ <div className="space-y-2 text-sm">
65
+ {Object.entries(dailyTotals).map(([date, total]) => (
66
+ <div key={date} className="flex items-center justify-between">
67
+ <span>{formatDate(date)}</span>
68
+ <span>{formatHours(total)}</span>
69
+ </div>
70
+ ))}
71
+ </div>
72
+ </SectionCard>
73
+ <SectionCard title={t('summary.weeklyTotal')}>
74
+ <p className="text-3xl font-semibold">{formatHours(weeklyTotal)}</p>
75
+ </SectionCard>
76
+ </div>
77
+
78
+ <SectionCard title={t('gridTitle')} description={t('gridDescription')}>
79
+ <div className="mb-4">
80
+ <Input
81
+ value={search}
82
+ onChange={(event) => setSearch(event.target.value)}
83
+ placeholder={t('searchPlaceholder')}
84
+ />
85
+ </div>
86
+ <Table>
87
+ <TableHeader>
88
+ <TableRow>
89
+ <TableHead>{t('columns.date')}</TableHead>
90
+ <TableHead>{t('columns.user')}</TableHead>
91
+ <TableHead>{t('columns.project')}</TableHead>
92
+ <TableHead>{t('columns.task')}</TableHead>
93
+ <TableHead>{t('columns.hours')}</TableHead>
94
+ <TableHead>{t('columns.description')}</TableHead>
95
+ <TableHead>{t('columns.status')}</TableHead>
96
+ </TableRow>
97
+ </TableHeader>
98
+ <TableBody>
99
+ {filteredTimesheets.map((entry) => {
100
+ const user = users.find((item) => item.id === entry.userId);
101
+ const project = projects.find((item) => item.id === entry.projectId);
102
+ const task = tasks.find((item) => item.id === entry.taskId);
103
+
104
+ return (
105
+ <TableRow key={entry.id}>
106
+ <TableCell>{formatDate(entry.date)}</TableCell>
107
+ <TableCell>{user?.name}</TableCell>
108
+ <TableCell>{project?.name}</TableCell>
109
+ <TableCell>{task?.title}</TableCell>
110
+ <TableCell>{formatHours(entry.hours)}</TableCell>
111
+ <TableCell>{entry.description}</TableCell>
112
+ <TableCell>
113
+ <StatusBadge
114
+ label={getApprovalLabel(entry.status)}
115
+ className={getApprovalBadgeClasses(entry.status)}
116
+ />
117
+ </TableCell>
118
+ </TableRow>
119
+ );
120
+ })}
121
+ </TableBody>
122
+ </Table>
123
+ </SectionCard>
124
+ </Page>
125
+ );
126
+ }