@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.
- package/dist/controllers/operations-tasks.controller.d.ts +4 -4
- package/dist/operations.service.d.ts +4 -4
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +24 -4
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +1 -1
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/dashboard_component_role.yaml +8 -8
- package/hedhog/data/dashboard_role.yaml +1 -1
- package/hedhog/data/menu.yaml +6 -16
- package/hedhog/data/role.yaml +1 -1
- package/hedhog/data/route.yaml +55 -55
- package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +15 -9
- package/hedhog/frontend/app/_components/project-costs-section.tsx.ejs +51 -81
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +39 -11
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +21 -4
- package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +10 -8
- package/hedhog/frontend/app/_lib/hooks/use-values-visibility.ts.ejs +61 -0
- package/hedhog/frontend/app/approvals/page.tsx.ejs +5 -1
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +68 -34
- package/hedhog/frontend/app/my-projects/page.tsx.ejs +45 -6
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/projects/page.tsx.ejs +60 -5
- package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +65 -52
- package/hedhog/frontend/app/reports/projects/page.tsx.ejs +80 -82
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +7 -1
- package/hedhog/frontend/messages/en.json +223 -16
- package/hedhog/frontend/messages/pt.json +223 -16
- package/package.json +4 -4
- package/src/operations.service.spec.ts +1 -1
- 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({
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
?
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
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
|
-
?
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
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
|
|
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={
|
|
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={(
|
|
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: '
|
|
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=
|
|
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
|
-
? '
|
|
279
|
-
: '
|
|
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 ? '
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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">
|
|
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>
|