@hed-hog/operations 0.0.329 → 0.0.330

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 (31) hide show
  1. package/dist/controllers/operations-tasks.controller.d.ts +4 -4
  2. package/dist/operations.service.d.ts +4 -4
  3. package/dist/operations.service.d.ts.map +1 -1
  4. package/dist/operations.service.js +24 -4
  5. package/dist/operations.service.js.map +1 -1
  6. package/dist/operations.service.spec.js +1 -1
  7. package/dist/operations.service.spec.js.map +1 -1
  8. package/hedhog/data/dashboard_component_role.yaml +8 -8
  9. package/hedhog/data/dashboard_role.yaml +1 -1
  10. package/hedhog/data/menu.yaml +6 -16
  11. package/hedhog/data/role.yaml +1 -1
  12. package/hedhog/data/route.yaml +55 -55
  13. package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +15 -9
  14. package/hedhog/frontend/app/_components/project-costs-section.tsx.ejs +51 -81
  15. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +39 -11
  16. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +21 -4
  17. package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +10 -8
  18. package/hedhog/frontend/app/_lib/hooks/use-values-visibility.ts.ejs +61 -0
  19. package/hedhog/frontend/app/approvals/page.tsx.ejs +5 -1
  20. package/hedhog/frontend/app/collaborators/page.tsx.ejs +68 -34
  21. package/hedhog/frontend/app/my-projects/page.tsx.ejs +45 -6
  22. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +1 -1
  23. package/hedhog/frontend/app/projects/page.tsx.ejs +60 -5
  24. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +65 -52
  25. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +80 -82
  26. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +7 -1
  27. package/hedhog/frontend/messages/en.json +223 -16
  28. package/hedhog/frontend/messages/pt.json +223 -16
  29. package/package.json +4 -4
  30. package/src/operations.service.spec.ts +1 -1
  31. package/src/operations.service.ts +25 -5
@@ -93,6 +93,7 @@ import {
93
93
  updateProjectCost,
94
94
  type Currency,
95
95
  } from '../_lib/api';
96
+ import { MASKED_VALUE } from '../_lib/hooks/use-values-visibility';
96
97
  import type {
97
98
  FullCostSummary,
98
99
  PaginatedResponse,
@@ -189,14 +190,23 @@ function costToForm(cost: ProjectCost): CostFormState {
189
190
 
190
191
  type Props = {
191
192
  projectId: number;
193
+ valuesVisible?: boolean;
192
194
  };
193
195
 
194
- export function ProjectCostsSection({ projectId }: Props) {
196
+ export function ProjectCostsSection({
197
+ projectId,
198
+ valuesVisible = false,
199
+ }: Props) {
195
200
  const t = useTranslations('operations.ProjectCostsSection');
196
201
  const commonT = useTranslations('operations.Common');
197
202
  const { request, showToastHandler, getSettingValue, currentLocaleCode } =
198
203
  useApp();
199
204
 
205
+ const fmtCurrency = (amount: number): string =>
206
+ valuesVisible
207
+ ? formatCurrency(amount, getSettingValue, currentLocaleCode)
208
+ : MASKED_VALUE;
209
+
200
210
  const queryClient = useQueryClient();
201
211
  const [isSheetOpen, setIsSheetOpen] = useState(false);
202
212
  const [editingCost, setEditingCost] = useState<ProjectCost | null>(null);
@@ -415,11 +425,7 @@ export function ProjectCostsSection({ projectId }: Props) {
415
425
  <p className="text-xs text-muted-foreground">
416
426
  {t('grandTotal')}:{' '}
417
427
  <span className="font-medium text-foreground">
418
- {formatCurrency(
419
- grandTotal,
420
- getSettingValue,
421
- currentLocaleCode
422
- )}
428
+ {fmtCurrency(grandTotal)}
423
429
  </span>
424
430
  </p>
425
431
  ) : null}
@@ -457,11 +463,7 @@ export function ProjectCostsSection({ projectId }: Props) {
457
463
  </span>
458
464
  </div>
459
465
  <p className="text-sm font-semibold tabular-nums">
460
- {formatCurrency(
461
- fullSummary.budget_amount,
462
- getSettingValue,
463
- currentLocaleCode
464
- )}
466
+ {fmtCurrency(fullSummary.budget_amount)}
465
467
  </p>
466
468
  </div>
467
469
  <div className="rounded-xl border bg-card p-3">
@@ -472,11 +474,7 @@ export function ProjectCostsSection({ projectId }: Props) {
472
474
  </span>
473
475
  </div>
474
476
  <p className="text-sm font-semibold tabular-nums">
475
- {formatCurrency(
476
- fullSummary.extra_cost_total,
477
- getSettingValue,
478
- currentLocaleCode
479
- )}
477
+ {fmtCurrency(fullSummary.extra_cost_total)}
480
478
  </p>
481
479
  </div>
482
480
  <div className="rounded-xl border bg-card p-3">
@@ -487,11 +485,7 @@ export function ProjectCostsSection({ projectId }: Props) {
487
485
  </span>
488
486
  </div>
489
487
  <p className="text-sm font-semibold tabular-nums">
490
- {formatCurrency(
491
- fullSummary.total_project_cost,
492
- getSettingValue,
493
- currentLocaleCode
494
- )}
488
+ {fmtCurrency(fullSummary.total_project_cost)}
495
489
  </p>
496
490
  </div>
497
491
  <div className="rounded-xl border bg-card p-3">
@@ -514,11 +508,7 @@ export function ProjectCostsSection({ projectId }: Props) {
514
508
  fullSummary.remaining_budget < 0 ? 'text-destructive' : ''
515
509
  )}
516
510
  >
517
- {formatCurrency(
518
- fullSummary.remaining_budget,
519
- getSettingValue,
520
- currentLocaleCode
521
- )}
511
+ {fmtCurrency(fullSummary.remaining_budget)}
522
512
  </p>
523
513
  </div>
524
514
  <div className="rounded-xl border bg-card p-3">
@@ -552,11 +542,7 @@ export function ProjectCostsSection({ projectId }: Props) {
552
542
  </span>
553
543
  </div>
554
544
  <p className="text-sm font-semibold tabular-nums">
555
- {formatCurrency(
556
- fullSummary.realized_total,
557
- getSettingValue,
558
- currentLocaleCode
559
- )}
545
+ {fmtCurrency(fullSummary.realized_total)}
560
546
  </p>
561
547
  </div>
562
548
  </div>
@@ -616,11 +602,7 @@ export function ProjectCostsSection({ projectId }: Props) {
616
602
  {entry.category_name}
617
603
  </span>
618
604
  <span className="ml-auto shrink-0 text-xs font-medium tabular-nums">
619
- {formatCurrency(
620
- entry.total,
621
- getSettingValue,
622
- currentLocaleCode
623
- )}
605
+ {fmtCurrency(entry.total)}
624
606
  </span>
625
607
  </div>
626
608
  ))}
@@ -846,11 +828,7 @@ export function ProjectCostsSection({ projectId }: Props) {
846
828
  </Badge>
847
829
  </div>
848
830
  <span className="text-xs font-medium text-muted-foreground">
849
- {formatCurrency(
850
- group.total_amount,
851
- getSettingValue,
852
- currentLocaleCode
853
- )}
831
+ {fmtCurrency(group.total_amount)}
854
832
  </span>
855
833
  </div>
856
834
 
@@ -920,20 +898,12 @@ export function ProjectCostsSection({ projectId }: Props) {
920
898
  <TableCell className="text-right tabular-nums">
921
899
  <span className="text-xs text-muted-foreground">
922
900
  {Number(cost.quantity ?? 1)} ×{' '}
923
- {formatCurrency(
924
- Number(cost.amount),
925
- getSettingValue,
926
- currentLocaleCode
927
- )}
901
+ {fmtCurrency(Number(cost.amount))}
928
902
  </span>
929
903
  </TableCell>
930
904
  <TableCell className="hidden text-right tabular-nums sm:table-cell">
931
905
  <span className="font-medium">
932
- {formatCurrency(
933
- total,
934
- getSettingValue,
935
- currentLocaleCode
936
- )}
906
+ {fmtCurrency(total)}
937
907
  </span>
938
908
  </TableCell>
939
909
  <TableCell>
@@ -1026,25 +996,25 @@ export function ProjectCostsSection({ projectId }: Props) {
1026
996
  placeholder: t('fields.costTypeCodePlaceholder'),
1027
997
  },
1028
998
  ]}
1029
- mapSearchToCreateValues={(search) => ({ name: search })}
1030
- onCreate={async (values) => {
1031
- const name = values.name?.trim() ?? '';
1032
- const slug = name
1033
- .toLowerCase()
1034
- .replace(/\s+/g, '-')
1035
- .replace(/[^a-z0-9-]/g, '');
1036
- const code =
1037
- values.code?.trim() ||
1038
- name
1039
- .toUpperCase()
1040
- .replace(/\s+/g, '-')
1041
- .replace(/[^A-Z0-9-]/g, '')
1042
- .slice(0, 40);
1043
- const created = await createProjectCostType(request, {
1044
- name,
1045
- code,
1046
- slug,
1047
- });
999
+ mapSearchToCreateValues={(search) => ({ name: search })}
1000
+ onCreate={async (values) => {
1001
+ const name = values.name?.trim() ?? '';
1002
+ const slug = name
1003
+ .toLowerCase()
1004
+ .replace(/\s+/g, '-')
1005
+ .replace(/[^a-z0-9-]/g, '');
1006
+ const code =
1007
+ values.code?.trim() ||
1008
+ name
1009
+ .toUpperCase()
1010
+ .replace(/\s+/g, '-')
1011
+ .replace(/[^A-Z0-9-]/g, '')
1012
+ .slice(0, 40);
1013
+ const created = await createProjectCostType(request, {
1014
+ name,
1015
+ code,
1016
+ slug,
1017
+ });
1048
1018
  return (created as ProjectCostType) ?? null;
1049
1019
  }}
1050
1020
  initialSelectedLabel={editingCost?.cost_type?.name}
@@ -1084,17 +1054,17 @@ export function ProjectCostsSection({ projectId }: Props) {
1084
1054
  required: true,
1085
1055
  },
1086
1056
  ]}
1087
- mapSearchToCreateValues={(search) => ({ name: search })}
1088
- onCreate={async (values) => {
1089
- const name = values.name?.trim() ?? '';
1090
- const slug = name
1091
- .toLowerCase()
1092
- .replace(/\s+/g, '-')
1093
- .replace(/[^a-z0-9-]/g, '');
1094
- const created = await createProjectCostCategory(request, {
1095
- name,
1096
- slug,
1097
- });
1057
+ mapSearchToCreateValues={(search) => ({ name: search })}
1058
+ onCreate={async (values) => {
1059
+ const name = values.name?.trim() ?? '';
1060
+ const slug = name
1061
+ .toLowerCase()
1062
+ .replace(/\s+/g, '-')
1063
+ .replace(/[^a-z0-9-]/g, '');
1064
+ const created = await createProjectCostCategory(request, {
1065
+ name,
1066
+ slug,
1067
+ });
1098
1068
  return (created as ProjectCostCategory) ?? null;
1099
1069
  }}
1100
1070
  initialSelectedLabel={editingCost?.category?.name}
@@ -80,6 +80,8 @@ import {
80
80
  CheckCircle2,
81
81
  ChevronRight,
82
82
  ClipboardList,
83
+ Eye,
84
+ EyeOff,
83
85
  FileText,
84
86
  FolderKanban,
85
87
  Gauge,
@@ -125,6 +127,10 @@ import {
125
127
  import { fetchOperations, mutateOperations } from '../_lib/api';
126
128
  import { useMentionItems } from '../_lib/hooks/use-mention-items';
127
129
  import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
130
+ import {
131
+ MASKED_VALUE,
132
+ useValuesVisibility,
133
+ } from '../_lib/hooks/use-values-visibility';
128
134
  import type {
129
135
  OperationsProjectDetails,
130
136
  OperationsTaskOption,
@@ -1100,6 +1106,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1100
1106
  const access = useOperationsAccess();
1101
1107
  const mentionItems = useMentionItems(request);
1102
1108
  const isLimitedView = !access.isDirector && !access.isSupervisor;
1109
+ const { valuesVisible, toggleValuesVisible } = useValuesVisibility();
1103
1110
  const router = useRouter();
1104
1111
  const pathname = usePathname();
1105
1112
  const searchParams = useSearchParams();
@@ -2078,6 +2085,20 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2078
2085
  </div>
2079
2086
  </div>
2080
2087
  <div className="flex shrink-0 flex-wrap gap-1.5">
2088
+ <Button
2089
+ size="icon"
2090
+ variant="ghost"
2091
+ onClick={toggleValuesVisible}
2092
+ title={commonT(
2093
+ valuesVisible ? 'actions.hideValues' : 'actions.showValues'
2094
+ )}
2095
+ >
2096
+ {valuesVisible ? (
2097
+ <EyeOff className="h-4 w-4" />
2098
+ ) : (
2099
+ <Eye className="h-4 w-4" />
2100
+ )}
2101
+ </Button>
2081
2102
  {access.isDirector ? (
2082
2103
  <Button
2083
2104
  size="sm"
@@ -2492,11 +2513,13 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2492
2513
  </dt>
2493
2514
  <dd className="font-medium">
2494
2515
  {project.budgetAmount
2495
- ? formatCurrency(
2496
- project.budgetAmount,
2497
- getSettingValue,
2498
- currentLocaleCode
2499
- )
2516
+ ? valuesVisible
2517
+ ? formatCurrency(
2518
+ project.budgetAmount,
2519
+ getSettingValue,
2520
+ currentLocaleCode
2521
+ )
2522
+ : MASKED_VALUE
2500
2523
  : commonT('labels.notAvailable')}
2501
2524
  </dd>
2502
2525
  </div>
@@ -2657,11 +2680,13 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2657
2680
  </dt>
2658
2681
  <dd className="font-medium">
2659
2682
  {project.relatedContract.budgetAmount
2660
- ? formatCurrency(
2661
- project.relatedContract.budgetAmount,
2662
- getSettingValue,
2663
- currentLocaleCode
2664
- )
2683
+ ? valuesVisible
2684
+ ? formatCurrency(
2685
+ project.relatedContract.budgetAmount,
2686
+ getSettingValue,
2687
+ currentLocaleCode
2688
+ )
2689
+ : MASKED_VALUE
2665
2690
  : commonT('labels.notAvailable')}
2666
2691
  </dd>
2667
2692
  </div>
@@ -4121,7 +4146,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
4121
4146
  description={t('sections.costsDescription')}
4122
4147
  className="rounded-2xl border bg-card p-4 shadow-sm"
4123
4148
  >
4124
- <ProjectCostsSection projectId={projectId} />
4149
+ <ProjectCostsSection
4150
+ projectId={projectId}
4151
+ valuesVisible={valuesVisible}
4152
+ />
4125
4153
  </SectionCard>
4126
4154
  </TabsContent>
4127
4155
  </Tabs>
@@ -111,6 +111,7 @@ type ProjectFormState = {
111
111
  managerCollaboratorId: string;
112
112
  code: string;
113
113
  name: string;
114
+ clientPersonId: string;
114
115
  clientName: string;
115
116
  summary: string;
116
117
  status: string;
@@ -173,6 +174,7 @@ function buildEmptyForm(
173
174
  managerCollaboratorId: 'none',
174
175
  code: '',
175
176
  name: '',
177
+ clientPersonId: '',
176
178
  clientName: '',
177
179
  summary: '',
178
180
  status: 'planning',
@@ -232,6 +234,9 @@ function toFormState(
232
234
  : 'none',
233
235
  code: project.code ?? '',
234
236
  name: project.name ?? '',
237
+ clientPersonId: project.clientPersonId
238
+ ? String(project.clientPersonId)
239
+ : '',
235
240
  clientName: project.clientName ?? '',
236
241
  summary: project.summary ?? '',
237
242
  status: project.status ?? 'planning',
@@ -651,6 +656,7 @@ export function ProjectFormScreen({
651
656
  managerCollaboratorId: z.string(),
652
657
  code: z.string().trim().min(1, t('messages.requiredFields')),
653
658
  name: z.string().trim().min(1, t('messages.requiredFields')),
659
+ clientPersonId: z.string(),
654
660
  clientName: z.string().trim().min(1, t('messages.requiredFields')),
655
661
  summary: z.string(),
656
662
  status: z.string(),
@@ -965,6 +971,9 @@ export function ProjectFormScreen({
965
971
  values.managerCollaboratorId === 'none'
966
972
  ? null
967
973
  : parseNumberInput(values.managerCollaboratorId),
974
+ clientPersonId: values.clientPersonId
975
+ ? parseNumberInput(values.clientPersonId)
976
+ : null,
968
977
  code: values.code.trim(),
969
978
  name: values.name.trim(),
970
979
  clientName: trimToNull(values.clientName),
@@ -1148,12 +1157,20 @@ export function ProjectFormScreen({
1148
1157
  <PersonPicker
1149
1158
  label=""
1150
1159
  entityLabel={t('fields.clientName')}
1151
- value={null}
1160
+ value={
1161
+ formMethods.watch('clientPersonId')
1162
+ ? Number(formMethods.watch('clientPersonId'))
1163
+ : null
1164
+ }
1152
1165
  initialSelectedLabel={field.value}
1153
1166
  selectPlaceholder={t('placeholders.clientName')}
1154
- onChange={(_, personName) =>
1155
- field.onChange(personName ?? '')
1156
- }
1167
+ onChange={(personId, personName) => {
1168
+ field.onChange(personName ?? '');
1169
+ formMethods.setValue(
1170
+ 'clientPersonId',
1171
+ personId ? String(personId) : ''
1172
+ );
1173
+ }}
1157
1174
  personTypeFilter="all"
1158
1175
  createType="company"
1159
1176
  />
@@ -17,6 +17,7 @@ import {
17
17
  Trash2,
18
18
  UploadCloud,
19
19
  } from 'lucide-react';
20
+ import { useTranslations } from 'next-intl';
20
21
  import { useCallback, useEffect, useRef, useState } from 'react';
21
22
 
22
23
  // ─── Types ────────────────────────────────────────────────────────────────────
@@ -117,6 +118,7 @@ type Props = {
117
118
  };
118
119
 
119
120
  export function TaskFileAttachments({ taskId }: Props) {
121
+ const t = useTranslations('operations.TaskFileAttachments');
120
122
  const { request } = useApp();
121
123
 
122
124
  const [files, setFiles] = useState<TaskFile[]>([]);
@@ -185,7 +187,7 @@ export function TaskFileAttachments({ taskId }: Props) {
185
187
  } catch {
186
188
  setUploading((prev) =>
187
189
  prev.map((u) =>
188
- u.key === key ? { ...u, error: 'Erro no upload' } : u
190
+ u.key === key ? { ...u, error: t('uploadError') } : u
189
191
  )
190
192
  );
191
193
  setTimeout(() => {
@@ -254,7 +256,7 @@ export function TaskFileAttachments({ taskId }: Props) {
254
256
  <div
255
257
  role="button"
256
258
  tabIndex={0}
257
- aria-label="Clique ou solte arquivos para anexar"
259
+ aria-label={t('dropzone.ariaLabel')}
258
260
  className={[
259
261
  'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed px-4 py-5 text-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
260
262
  isDraggingOver
@@ -275,8 +277,8 @@ export function TaskFileAttachments({ taskId }: Props) {
275
277
  <UploadCloud className="size-6 shrink-0 opacity-60" />
276
278
  <span className="text-center text-xs leading-relaxed">
277
279
  {isDraggingOver
278
- ? 'Solte os arquivos aqui'
279
- : 'Clique ou arraste arquivos para anexar'}
280
+ ? t('dropzone.dragging')
281
+ : t('dropzone.idle')}
280
282
  </span>
281
283
  <input
282
284
  ref={inputRef}
@@ -308,7 +310,7 @@ export function TaskFileAttachments({ taskId }: Props) {
308
310
  )}
309
311
  </div>
310
312
  <span className="shrink-0 text-[10px] tabular-nums text-muted-foreground">
311
- {u.error ? 'Erro' : `${u.progress}%`}
313
+ {u.error ? t('error') : `${u.progress}%`}
312
314
  </span>
313
315
  </div>
314
316
  ))}
@@ -329,7 +331,7 @@ export function TaskFileAttachments({ taskId }: Props) {
329
331
  {!loadingFiles && isEmpty ? (
330
332
  <div className="flex items-center gap-2 rounded-xl border border-dashed bg-muted/10 px-3 py-3 text-xs text-muted-foreground">
331
333
  <Paperclip className="size-3.5 shrink-0" />
332
- Nenhum arquivo anexado
334
+ {t('empty')}
333
335
  </div>
334
336
  ) : null}
335
337
 
@@ -357,7 +359,7 @@ export function TaskFileAttachments({ taskId }: Props) {
357
359
  variant="ghost"
358
360
  size="icon"
359
361
  className="size-7 rounded-lg"
360
- title="Abrir em nova aba"
362
+ title={t('actions.openInNewTab')}
361
363
  onClick={() =>
362
364
  window.open(getFileOpenUrl(file.file_id), '_blank')
363
365
  }
@@ -369,7 +371,7 @@ export function TaskFileAttachments({ taskId }: Props) {
369
371
  variant="ghost"
370
372
  size="icon"
371
373
  className="size-7 rounded-lg text-destructive hover:bg-destructive/10 hover:text-destructive"
372
- title="Remover arquivo"
374
+ title={t('actions.removeFile')}
373
375
  disabled={isDeleting}
374
376
  onClick={() => void handleDelete(file.id)}
375
377
  >
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+
5
+ /**
6
+ * Global sessionStorage key shared across all Operations screens.
7
+ * Changing this constant will reset everyone's saved preference.
8
+ */
9
+ export const OPERATIONS_VALUES_VISIBILITY_KEY = 'operations-values-visible';
10
+
11
+ /**
12
+ * Convenience helper — returns the masked placeholder string.
13
+ * Use wherever a monetary value should be hidden.
14
+ */
15
+ export const MASKED_VALUE = '••••••';
16
+
17
+ /**
18
+ * useValuesVisibility — toggles monetary-value visibility for Operations screens.
19
+ *
20
+ * State is stored in sessionStorage under OPERATIONS_VALUES_VISIBILITY_KEY so it
21
+ * persists across page navigations within the same browser tab but resets when the
22
+ * tab is closed (privacy-first default).
23
+ *
24
+ * Default: hidden (false).
25
+ *
26
+ * Usage:
27
+ * const { valuesVisible, toggleValuesVisible } = useValuesVisibility();
28
+ *
29
+ * // In JSX (eye button):
30
+ * <Button size="icon" variant="ghost" onClick={toggleValuesVisible}>
31
+ * {valuesVisible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
32
+ * </Button>
33
+ *
34
+ * // Masking a value:
35
+ * value={valuesVisible ? formatCurrency(amount, ...) : MASKED_VALUE}
36
+ */
37
+ export function useValuesVisibility() {
38
+ const [valuesVisible, setValuesVisible] = useState<boolean>(() => {
39
+ try {
40
+ return (
41
+ sessionStorage.getItem(OPERATIONS_VALUES_VISIBILITY_KEY) === 'true'
42
+ );
43
+ } catch {
44
+ return false;
45
+ }
46
+ });
47
+
48
+ const toggleValuesVisible = useCallback(() => {
49
+ setValuesVisible((prev) => {
50
+ const next = !prev;
51
+ try {
52
+ sessionStorage.setItem(OPERATIONS_VALUES_VISIBILITY_KEY, String(next));
53
+ } catch {
54
+ // ignore — storage may be unavailable (e.g. private browsing restrictions)
55
+ }
56
+ return next;
57
+ });
58
+ }, []);
59
+
60
+ return { valuesVisible, toggleValuesVisible };
61
+ }
@@ -146,10 +146,12 @@ function SchedulePanel({
146
146
  days,
147
147
  locale,
148
148
  emptyLabel,
149
+ dayOffLabel,
149
150
  }: {
150
151
  days: ScheduleDay[];
151
152
  locale: string;
152
153
  emptyLabel: string;
154
+ dayOffLabel: string;
153
155
  }) {
154
156
  const lines = groupScheduleLines(days, locale);
155
157
  if (!lines.length)
@@ -167,7 +169,7 @@ function SchedulePanel({
167
169
  {line.dayLabel}
168
170
  </span>
169
171
  {line.isOff ? (
170
- <span className="text-muted-foreground">Folga</span>
172
+ <span className="text-muted-foreground">{dayOffLabel}</span>
171
173
  ) : (
172
174
  <span className="text-foreground">
173
175
  {line.time}
@@ -1176,6 +1178,7 @@ export default function OperationsApprovalsPage() {
1176
1178
  days={selectedApproval.currentSchedule ?? []}
1177
1179
  locale={currentLocaleCode}
1178
1180
  emptyLabel={commonT('labels.notAssigned')}
1181
+ dayOffLabel={commonT('labels.dayOff')}
1179
1182
  />
1180
1183
  </div>
1181
1184
  </div>
@@ -1188,6 +1191,7 @@ export default function OperationsApprovalsPage() {
1188
1191
  days={selectedApproval.days ?? []}
1189
1192
  locale={currentLocaleCode}
1190
1193
  emptyLabel={commonT('labels.notAssigned')}
1194
+ dayOffLabel={commonT('labels.dayOff')}
1191
1195
  />
1192
1196
  </div>
1193
1197
  </div>