@hed-hog/operations 0.0.301 → 0.0.303

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.
@@ -2,7 +2,6 @@
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';
6
5
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
7
6
  import {
8
7
  Sheet,
@@ -51,10 +50,13 @@ export default function OperationsDepartmentsPage() {
51
50
  const { request, showToastHandler, currentLocaleCode } = useApp();
52
51
  const access = useOperationsAccess();
53
52
  const [search, setSearch] = useState('');
54
- const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
53
+ const [statusFilter, setStatusFilter] = useState<
54
+ 'all' | 'active' | 'inactive'
55
+ >('all');
55
56
  const [isSheetOpen, setIsSheetOpen] = useState(false);
56
57
  const [isSaving, setIsSaving] = useState(false);
57
- const [editingDepartment, setEditingDepartment] = useState<OperationsDepartment | null>(null);
58
+ const [editingDepartment, setEditingDepartment] =
59
+ useState<OperationsDepartment | null>(null);
58
60
  const [form, setForm] = useState<DepartmentFormState>(EMPTY_FORM_STATE);
59
61
 
60
62
  const {
@@ -65,7 +67,10 @@ export default function OperationsDepartmentsPage() {
65
67
  queryKey: ['operations-departments-list', currentLocaleCode],
66
68
  enabled: access.isDirector,
67
69
  queryFn: () =>
68
- fetchOperations<OperationsDepartment[]>(request, '/operations/departments'),
70
+ fetchOperations<OperationsDepartment[]>(
71
+ request,
72
+ '/operations/departments'
73
+ ),
69
74
  });
70
75
 
71
76
  const filteredDepartments = useMemo(
@@ -210,7 +215,9 @@ export default function OperationsDepartmentsPage() {
210
215
  current={t('breadcrumb')}
211
216
  actions={
212
217
  <Button variant="outline" size="sm" asChild>
213
- <Link href="/operations/collaborators">{commonT('actions.back')}</Link>
218
+ <Link href="/operations/collaborators">
219
+ {commonT('actions.back')}
220
+ </Link>
214
221
  </Button>
215
222
  }
216
223
  />
@@ -237,7 +244,9 @@ export default function OperationsDepartmentsPage() {
237
244
  actions={
238
245
  <div className="flex flex-wrap gap-2">
239
246
  <Button variant="outline" size="sm" asChild>
240
- <Link href="/operations/collaborators">{commonT('actions.back')}</Link>
247
+ <Link href="/operations/collaborators">
248
+ {commonT('actions.back')}
249
+ </Link>
241
250
  </Button>
242
251
  <Button size="sm" onClick={openCreateSheet}>
243
252
  {commonT('actions.create')}
@@ -260,7 +269,9 @@ export default function OperationsDepartmentsPage() {
260
269
  type: 'select',
261
270
  value: statusFilter,
262
271
  onChange: (value) =>
263
- setStatusFilter((value as 'all' | 'active' | 'inactive') ?? 'all'),
272
+ setStatusFilter(
273
+ (value as 'all' | 'active' | 'inactive') ?? 'all'
274
+ ),
264
275
  placeholder: commonT('labels.status'),
265
276
  options: [
266
277
  { value: 'all', label: commonT('filters.allStatuses') },
@@ -273,70 +284,70 @@ export default function OperationsDepartmentsPage() {
273
284
  </div>
274
285
 
275
286
  {isLoading ? (
276
- <Card className="py-0">
277
- <CardContent className="p-6 text-sm text-muted-foreground">
278
- {commonT('actions.refresh')}...
279
- </CardContent>
280
- </Card>
287
+ <div className="rounded-md border px-4 py-6 text-sm text-muted-foreground">
288
+ {commonT('actions.refresh')}...
289
+ </div>
281
290
  ) : filteredDepartments.length > 0 ? (
282
- <Card className="py-0">
283
- <CardContent className="p-0">
284
- <div className="overflow-x-auto rounded-md border">
285
- <Table>
286
- <TableHeader>
287
- <TableRow>
288
- <TableHead>{t('columns.name')}</TableHead>
289
- <TableHead>{t('columns.code')}</TableHead>
290
- <TableHead>{t('columns.description')}</TableHead>
291
- <TableHead>{t('columns.collaborators')}</TableHead>
292
- <TableHead>{commonT('labels.status')}</TableHead>
293
- <TableHead className="text-right">{commonT('labels.actions')}</TableHead>
294
- </TableRow>
295
- </TableHeader>
296
- <TableBody>
297
- {filteredDepartments.map((department) => (
298
- <TableRow key={department.id}>
299
- <TableCell className="font-medium">{department.name}</TableCell>
300
- <TableCell>{department.code || '—'}</TableCell>
301
- <TableCell className="max-w-md text-sm text-muted-foreground">
302
- {department.description || commonT('labels.notAvailable')}
303
- </TableCell>
304
- <TableCell>{Number(department.collaboratorCount ?? 0)}</TableCell>
305
- <TableCell>
306
- <StatusBadge
307
- label={formatEnumLabel(department.status)}
308
- className={getStatusBadgeClass(department.status)}
309
- />
310
- </TableCell>
311
- <TableCell>
312
- <div className="flex justify-end gap-2">
313
- <Button
314
- variant="outline"
315
- size="icon"
316
- className="cursor-pointer"
317
- onClick={() => openEditSheet(department)}
318
- >
319
- <Pencil className="size-4" />
320
- </Button>
321
- <Button
322
- variant="outline"
323
- size="sm"
324
- className="cursor-pointer"
325
- onClick={() => void toggleStatus(department)}
326
- >
327
- {department.status === 'inactive'
328
- ? commonT('actions.activate')
329
- : commonT('actions.deactivate')}
330
- </Button>
331
- </div>
332
- </TableCell>
333
- </TableRow>
334
- ))}
335
- </TableBody>
336
- </Table>
337
- </div>
338
- </CardContent>
339
- </Card>
291
+ <div className="overflow-x-auto rounded-md border">
292
+ <Table>
293
+ <TableHeader>
294
+ <TableRow>
295
+ <TableHead>{t('columns.name')}</TableHead>
296
+ <TableHead>{t('columns.code')}</TableHead>
297
+ <TableHead>{t('columns.description')}</TableHead>
298
+ <TableHead>{t('columns.collaborators')}</TableHead>
299
+ <TableHead>{commonT('labels.status')}</TableHead>
300
+ <TableHead className="text-right">
301
+ {commonT('labels.actions')}
302
+ </TableHead>
303
+ </TableRow>
304
+ </TableHeader>
305
+ <TableBody>
306
+ {filteredDepartments.map((department) => (
307
+ <TableRow key={department.id}>
308
+ <TableCell className="font-medium">
309
+ {department.name}
310
+ </TableCell>
311
+ <TableCell>{department.code || ''}</TableCell>
312
+ <TableCell className="max-w-md text-sm text-muted-foreground">
313
+ {department.description || commonT('labels.notAvailable')}
314
+ </TableCell>
315
+ <TableCell>
316
+ {Number(department.collaboratorCount ?? 0)}
317
+ </TableCell>
318
+ <TableCell>
319
+ <StatusBadge
320
+ label={formatEnumLabel(department.status)}
321
+ className={getStatusBadgeClass(department.status)}
322
+ />
323
+ </TableCell>
324
+ <TableCell>
325
+ <div className="flex justify-end gap-2">
326
+ <Button
327
+ variant="outline"
328
+ size="icon"
329
+ className="cursor-pointer"
330
+ onClick={() => openEditSheet(department)}
331
+ >
332
+ <Pencil className="size-4" />
333
+ </Button>
334
+ <Button
335
+ variant="outline"
336
+ size="sm"
337
+ className="cursor-pointer"
338
+ onClick={() => void toggleStatus(department)}
339
+ >
340
+ {department.status === 'inactive'
341
+ ? commonT('actions.activate')
342
+ : commonT('actions.deactivate')}
343
+ </Button>
344
+ </div>
345
+ </TableCell>
346
+ </TableRow>
347
+ ))}
348
+ </TableBody>
349
+ </Table>
350
+ </div>
340
351
  ) : (
341
352
  <EmptyState
342
353
  icon={<Building2 className="size-12" />}
@@ -351,7 +362,9 @@ export default function OperationsDepartmentsPage() {
351
362
  <SheetContent className="w-full overflow-y-auto sm:max-w-xl">
352
363
  <SheetHeader>
353
364
  <SheetTitle>
354
- {editingDepartment ? t('sheet.editTitle') : t('sheet.createTitle')}
365
+ {editingDepartment
366
+ ? t('sheet.editTitle')
367
+ : t('sheet.createTitle')}
355
368
  </SheetTitle>
356
369
  <SheetDescription>{t('sheet.description')}</SheetDescription>
357
370
  </SheetHeader>
@@ -390,20 +403,28 @@ export default function OperationsDepartmentsPage() {
390
403
  </div>
391
404
 
392
405
  <div className="space-y-2">
393
- <label className="text-sm font-medium" htmlFor="department-description">
406
+ <label
407
+ className="text-sm font-medium"
408
+ htmlFor="department-description"
409
+ >
394
410
  {t('form.description')}
395
411
  </label>
396
412
  <textarea
397
413
  id="department-description"
398
414
  value={form.description}
399
- onChange={(event) => updateForm('description', event.target.value)}
415
+ onChange={(event) =>
416
+ updateForm('description', event.target.value)
417
+ }
400
418
  className="min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
401
419
  placeholder={t('form.description')}
402
420
  />
403
421
  </div>
404
422
 
405
423
  <div className="space-y-2">
406
- <label className="text-sm font-medium" htmlFor="department-status">
424
+ <label
425
+ className="text-sm font-medium"
426
+ htmlFor="department-status"
427
+ >
407
428
  {t('form.status')}
408
429
  </label>
409
430
  <select
@@ -1,343 +1,5 @@
1
- 'use client';
1
+ import { redirect } from 'next/navigation';
2
2
 
3
- import { EmptyState, Page } from '@/components/entity-list';
4
- import { Button } from '@/components/ui/button';
5
- import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
6
- import { Skeleton } from '@/components/ui/skeleton';
7
- import {
8
- Table,
9
- TableBody,
10
- TableCell,
11
- TableHead,
12
- TableHeader,
13
- TableRow,
14
- } from '@/components/ui/table';
15
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
16
- import {
17
- Building2,
18
- CheckCircle2,
19
- ClipboardList,
20
- FolderKanban,
21
- Hourglass,
22
- PlaneTakeoff,
23
- RefreshCcw,
24
- Users,
25
- } from 'lucide-react';
26
- import { useTranslations } from 'next-intl';
27
- import Link from 'next/link';
28
- import { OperationsHeader } from './_components/operations-header';
29
- import { SectionCard } from './_components/section-card';
30
- import { StatusBadge } from './_components/status-badge';
31
- import { fetchOperations } from './_lib/api';
32
- import { useOperationsAccess } from './_lib/hooks/use-operations-access';
33
- import type { OperationsDashboard, OperationsTeamOverview } from './_lib/types';
34
- import {
35
- formatDateRange,
36
- formatEnumLabel,
37
- formatHours,
38
- getStatusBadgeClass,
39
- } from './_lib/utils/format';
40
-
41
- export default function OperationsDashboardPage() {
42
- const t = useTranslations('operations.DashboardPage');
43
- const commonT = useTranslations('operations.Common');
44
- const { request, currentLocaleCode } = useApp();
45
- const access = useOperationsAccess();
46
-
47
- const {
48
- data: dashboard,
49
- isLoading,
50
- refetch,
51
- } = useQuery<OperationsDashboard>({
52
- queryKey: ['operations-dashboard', currentLocaleCode],
53
- queryFn: () =>
54
- fetchOperations<OperationsDashboard>(request, '/operations/dashboard'),
55
- });
56
-
57
- const { data: team } = useQuery<OperationsTeamOverview>({
58
- queryKey: ['operations-team-overview', currentLocaleCode],
59
- enabled: access.isSupervisor,
60
- queryFn: () =>
61
- fetchOperations<OperationsTeamOverview>(request, '/operations/team'),
62
- });
63
-
64
- const cards = dashboard
65
- ? [
66
- {
67
- key: 'projects',
68
- title: t('cards.projects'),
69
- value: dashboard.cards.projectsTotal,
70
- description: t('cards.projectsDescription', {
71
- active: dashboard.cards.activeProjects,
72
- }),
73
- icon: FolderKanban,
74
- },
75
- {
76
- key: 'timesheets',
77
- title: t('cards.timesheets'),
78
- value: dashboard.cards.visibleTimesheets,
79
- description: t('cards.timesheetsDescription', {
80
- pending: dashboard.cards.pendingTimesheets,
81
- }),
82
- icon: ClipboardList,
83
- },
84
- {
85
- key: 'timeOff',
86
- title: t('cards.timeOff'),
87
- value: dashboard.cards.timeOffRequests,
88
- description: t('cards.timeOffDescription'),
89
- icon: PlaneTakeoff,
90
- },
91
- {
92
- key: 'approvals',
93
- title: t('cards.approvals'),
94
- value: dashboard.cards.pendingApprovals,
95
- description: t('cards.approvalsDescription'),
96
- icon: Hourglass,
97
- },
98
- ]
99
- : [];
100
-
101
- return (
102
- <Page>
103
- <OperationsHeader
104
- title={t('title')}
105
- description={t('description')}
106
- current={t('breadcrumb')}
107
- actions={
108
- <div className="flex flex-wrap gap-2">
109
- <Button variant="outline" size="sm" onClick={() => void refetch()}>
110
- <RefreshCcw className="size-4" />
111
- {commonT('actions.refresh')}
112
- </Button>
113
- {access.isDirector ? (
114
- <Button variant="outline" size="sm" asChild>
115
- <Link href="/operations/departments">
116
- <Building2 className="size-4" />
117
- {commonT('actions.manageDepartments')}
118
- </Link>
119
- </Button>
120
- ) : null}
121
- <Button asChild size="sm">
122
- <Link href="/operations/timesheets">
123
- {t('actions.openTimesheets')}
124
- </Link>
125
- </Button>
126
- </div>
127
- }
128
- />
129
-
130
- {isLoading || access.isLoading ? (
131
- <div className="space-y-4">
132
- <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
133
- {Array.from({ length: 4 }).map((_, index) => (
134
- <Skeleton key={index} className="h-32 w-full" />
135
- ))}
136
- </div>
137
- <div className="grid gap-4 xl:grid-cols-3">
138
- <Skeleton className="h-64 w-full xl:col-span-2" />
139
- <Skeleton className="h-64 w-full" />
140
- </div>
141
- </div>
142
- ) : dashboard ? (
143
- <>
144
- <KpiCardsGrid items={cards} />
145
-
146
- <div className="grid gap-4 xl:grid-cols-3">
147
- <SectionCard
148
- title={t('scope.title')}
149
- description={t('scope.description')}
150
- className="xl:col-span-1"
151
- >
152
- <dl className="space-y-3 text-sm">
153
- <div className="flex items-center justify-between gap-4">
154
- <dt className="text-muted-foreground">
155
- {t('scope.roleScope')}
156
- </dt>
157
- <dd className="font-medium">
158
- {formatEnumLabel(dashboard.actor.roleScope)}
159
- </dd>
160
- </div>
161
- <div className="flex items-center justify-between gap-4">
162
- <dt className="text-muted-foreground">
163
- {t('scope.collaborator')}
164
- </dt>
165
- <dd className="font-medium">
166
- {dashboard.actor.collaboratorName ??
167
- commonT('labels.notAvailable')}
168
- </dd>
169
- </div>
170
- <div className="flex items-center justify-between gap-4">
171
- <dt className="text-muted-foreground">
172
- {t('scope.teamSize')}
173
- </dt>
174
- <dd className="font-medium">{dashboard.actor.teamSize}</dd>
175
- </div>
176
- </dl>
177
- </SectionCard>
178
-
179
- <SectionCard
180
- title={t('recentTimesheets.title')}
181
- description={t('recentTimesheets.description')}
182
- className="xl:col-span-2"
183
- >
184
- {dashboard.recentTimesheets.length === 0 ? (
185
- <EmptyState
186
- icon={<ClipboardList className="size-12" />}
187
- title={commonT('states.emptyTitle')}
188
- description={t('recentTimesheets.empty')}
189
- actionLabel={t('actions.openTimesheets')}
190
- onAction={() => {
191
- window.location.href = '/operations/timesheets';
192
- }}
193
- />
194
- ) : (
195
- <div className="overflow-x-auto rounded-md border">
196
- <Table>
197
- <TableHeader>
198
- <TableRow>
199
- <TableHead>{commonT('labels.collaborator')}</TableHead>
200
- <TableHead>{commonT('labels.week')}</TableHead>
201
- <TableHead>{commonT('labels.totalHours')}</TableHead>
202
- <TableHead>{commonT('labels.status')}</TableHead>
203
- </TableRow>
204
- </TableHeader>
205
- <TableBody>
206
- {dashboard.recentTimesheets.map((item) => (
207
- <TableRow key={item.id}>
208
- <TableCell className="font-medium">
209
- {item.collaboratorName}
210
- </TableCell>
211
- <TableCell>
212
- {formatDateRange(
213
- item.weekStartDate,
214
- item.weekEndDate
215
- )}
216
- </TableCell>
217
- <TableCell>{formatHours(item.totalHours)}</TableCell>
218
- <TableCell>
219
- <StatusBadge
220
- label={formatEnumLabel(item.status)}
221
- className={getStatusBadgeClass(item.status)}
222
- />
223
- </TableCell>
224
- </TableRow>
225
- ))}
226
- </TableBody>
227
- </Table>
228
- </div>
229
- )}
230
- </SectionCard>
231
- </div>
232
-
233
- <div className="grid gap-4 xl:grid-cols-2">
234
- <SectionCard
235
- title={t('nextSteps.title')}
236
- description={t('nextSteps.description')}
237
- >
238
- <div className="space-y-3 text-sm">
239
- <div className="flex items-start gap-3">
240
- <CheckCircle2 className="mt-0.5 size-4 text-emerald-600" />
241
- <p>{t('nextSteps.submitTimesheet')}</p>
242
- </div>
243
- <div className="flex items-start gap-3">
244
- <CheckCircle2 className="mt-0.5 size-4 text-emerald-600" />
245
- <p>{t('nextSteps.requestTimeOff')}</p>
246
- </div>
247
- <div className="flex items-start gap-3">
248
- <CheckCircle2 className="mt-0.5 size-4 text-emerald-600" />
249
- <p>{t('nextSteps.adjustSchedule')}</p>
250
- </div>
251
- </div>
252
- </SectionCard>
253
-
254
- <SectionCard
255
- title={t('team.title')}
256
- description={t('team.description')}
257
- >
258
- {access.isSupervisor && team ? (
259
- <>
260
- <div className="mb-4 grid gap-3 md:grid-cols-3">
261
- <div className="rounded-lg border p-4">
262
- <div className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
263
- {t('team.members')}
264
- </div>
265
- <div className="mt-2 text-2xl font-semibold">
266
- {team.teamMembers.length}
267
- </div>
268
- </div>
269
- <div className="rounded-lg border p-4">
270
- <div className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
271
- {t('team.projects')}
272
- </div>
273
- <div className="mt-2 text-2xl font-semibold">
274
- {team.projectCount}
275
- </div>
276
- </div>
277
- <div className="rounded-lg border p-4">
278
- <div className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
279
- {t('team.pendingApprovals')}
280
- </div>
281
- <div className="mt-2 text-2xl font-semibold">
282
- {team.pendingApprovals}
283
- </div>
284
- </div>
285
- </div>
286
-
287
- {team.teamMembers.length > 0 ? (
288
- <div className="space-y-3">
289
- {team.teamMembers.slice(0, 4).map((member) => (
290
- <div
291
- key={member.id}
292
- className="flex items-center justify-between rounded-lg border px-4 py-3 text-sm"
293
- >
294
- <div>
295
- <div className="font-medium">
296
- {member.displayName}
297
- </div>
298
- <div className="text-muted-foreground">
299
- {[member.department, member.title]
300
- .filter(Boolean)
301
- .join(' • ') || commonT('labels.notAvailable')}
302
- </div>
303
- </div>
304
- <div className="text-right">
305
- <div className="font-medium">
306
- {member.activeAssignments ?? 0}{' '}
307
- {t('team.assignments')}
308
- </div>
309
- <div className="text-muted-foreground">
310
- {member.pendingApprovals ?? 0}{' '}
311
- {t('team.awaitingReview')}
312
- </div>
313
- </div>
314
- </div>
315
- ))}
316
- </div>
317
- ) : (
318
- <p className="text-sm text-muted-foreground">
319
- {t('team.empty')}
320
- </p>
321
- )}
322
- </>
323
- ) : (
324
- <div className="flex items-start gap-3 rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
325
- <Users className="mt-0.5 size-4" />
326
- <p>{t('team.collaboratorMessage')}</p>
327
- </div>
328
- )}
329
- </SectionCard>
330
- </div>
331
- </>
332
- ) : (
333
- <EmptyState
334
- icon={<FolderKanban className="size-12" />}
335
- title={commonT('states.emptyTitle')}
336
- description={commonT('states.emptyDescription')}
337
- actionLabel={commonT('actions.refresh')}
338
- onAction={() => void refetch()}
339
- />
340
- )}
341
- </Page>
342
- );
3
+ export default function OperationsPage() {
4
+ redirect('/operations/projects');
343
5
  }