@ossy/timesheets 3.7.1 → 3.9.0
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/package.json +6 -3
- package/src/HolidayLocaleSelect.jsx +32 -0
- package/src/Timesheet.jsx +97 -21
- package/src/TimesheetCard.jsx +57 -18
- package/src/TimesheetExportCard.jsx +71 -16
- package/src/TimesheetPanel.jsx +249 -0
- package/src/TimesheetPartyFields.jsx +121 -0
- package/src/TimesheetPartyRows.jsx +101 -0
- package/src/TimesheetsProductHome.jsx +268 -70
- package/src/delete.action.js +5 -0
- package/src/delete.task.js +42 -0
- package/src/download-timesheet-csv.js +20 -1
- package/src/en.translations.json +25 -4
- package/src/generate.task.js +12 -33
- package/src/get.task.js +1 -1
- package/src/holiday-locale.js +6 -64
- package/src/index.js +4 -2
- package/src/list.task.js +5 -1
- package/src/save.task.js +16 -5
- package/src/schema-ids.js +6 -0
- package/src/sv.translations.json +25 -4
- package/src/timesheet-detail.page.jsx +10 -193
- package/src/timesheet-parties.js +57 -0
- package/src/timesheet-parties.spec.js +40 -0
- package/src/timesheet-resources.js +23 -1
- package/src/timesheet-resources.spec.js +59 -0
- package/src/timesheet.schema.js +15 -1
package/package.json
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/timesheets",
|
|
3
3
|
"description": "Timesheets — generate, review, save, and export monthly work hours",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.9.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.js"
|
|
10
10
|
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose"
|
|
13
|
+
},
|
|
11
14
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
12
15
|
"license": "MIT",
|
|
13
16
|
"ossy": {
|
|
14
17
|
"src": "./src"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@ossy/resources": "^3.
|
|
20
|
+
"@ossy/resources": "^3.9.0",
|
|
18
21
|
"html-to-image": "^1.11.13",
|
|
19
22
|
"nanoid": "^5.1.11",
|
|
20
23
|
"pdf-lib": "^1.17.1"
|
|
@@ -38,5 +41,5 @@
|
|
|
38
41
|
"/src",
|
|
39
42
|
"README.md"
|
|
40
43
|
],
|
|
41
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "f404be69becb27a1fd853a6ff1903554e76e7d17"
|
|
42
45
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import { Select, Text, useLocale, View } from '@ossy/design-system'
|
|
3
|
+
import { HOLIDAY_COUNTRY_CODES } from '@ossy/calendar/holiday-locale'
|
|
4
|
+
|
|
5
|
+
export function HolidayLocaleSelect ({ value, onChange, disabled }) {
|
|
6
|
+
const { language, t } = useLocale()
|
|
7
|
+
const displayNames = useMemo(() => {
|
|
8
|
+
try {
|
|
9
|
+
return new Intl.DisplayNames([language || 'en'], { type: 'region' })
|
|
10
|
+
} catch {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
}, [language])
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<View gap="xs">
|
|
17
|
+
<Text variant="small" weight="medium" text="timesheets.holidayLocale" />
|
|
18
|
+
<Select
|
|
19
|
+
value={value}
|
|
20
|
+
onChange={onChange}
|
|
21
|
+
disabled={disabled}
|
|
22
|
+
aria-label={t('timesheets.holidayLocale')}
|
|
23
|
+
>
|
|
24
|
+
{HOLIDAY_COUNTRY_CODES.map((code) => (
|
|
25
|
+
<option key={code} value={code}>
|
|
26
|
+
{displayNames?.of(code) || code}
|
|
27
|
+
</option>
|
|
28
|
+
))}
|
|
29
|
+
</Select>
|
|
30
|
+
</View>
|
|
31
|
+
)
|
|
32
|
+
}
|
package/src/Timesheet.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useMemo } from 'react'
|
|
2
2
|
import { View, Text, Input, DateDisplay, Separator, useLocale } from '@ossy/design-system'
|
|
3
3
|
import { buildMonthGrid, dateKeyFromMs } from '@ossy/calendar'
|
|
4
|
+
import { TimesheetPartyRows } from './TimesheetPartyRows.jsx'
|
|
4
5
|
|
|
5
6
|
const calendarGrid = {
|
|
6
7
|
display: 'grid',
|
|
@@ -13,25 +14,39 @@ const calendarGrid = {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const dayCell = {
|
|
17
|
+
position: 'relative',
|
|
16
18
|
boxSizing: 'border-box',
|
|
17
19
|
display: 'flex',
|
|
18
|
-
flexDirection: 'column',
|
|
19
20
|
alignItems: 'center',
|
|
20
|
-
justifyContent: '
|
|
21
|
-
gap: 4,
|
|
21
|
+
justifyContent: 'center',
|
|
22
22
|
width: '100%',
|
|
23
23
|
minHeight: 72,
|
|
24
|
+
margin: 0,
|
|
24
25
|
borderRadius: 'var(--space-xxs, 2px)',
|
|
25
|
-
padding: '
|
|
26
|
+
padding: '18px 4px 8px',
|
|
27
|
+
cursor: 'text',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const dateNumber = {
|
|
31
|
+
position: 'absolute',
|
|
32
|
+
top: 4,
|
|
33
|
+
right: 5,
|
|
34
|
+
fontSize: 11,
|
|
35
|
+
fontWeight: 500,
|
|
36
|
+
lineHeight: 1,
|
|
37
|
+
letterSpacing: 0,
|
|
38
|
+
fontFamily: 'var(--text-default-font-family, sans-serif)',
|
|
39
|
+
pointerEvents: 'none',
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
const hoursInput = {
|
|
29
43
|
width: '100%',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
padding: '6px 4px',
|
|
44
|
+
minHeight: 28,
|
|
45
|
+
padding: 0,
|
|
33
46
|
textAlign: 'center',
|
|
34
|
-
fontSize:
|
|
47
|
+
fontSize: 14,
|
|
48
|
+
fontWeight: 700,
|
|
49
|
+
lineHeight: 1,
|
|
35
50
|
borderRadius: 'var(--space-xxs, 2px)',
|
|
36
51
|
background: 'transparent',
|
|
37
52
|
borderColor: 'transparent',
|
|
@@ -39,6 +54,8 @@ const hoursInput = {
|
|
|
39
54
|
borderStyle: 'solid',
|
|
40
55
|
boxShadow: 'none',
|
|
41
56
|
outline: 'none',
|
|
57
|
+
appearance: 'textfield',
|
|
58
|
+
MozAppearance: 'textfield',
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
const WEEKDAY_KEYS = [
|
|
@@ -56,7 +73,17 @@ const WEEKDAY_KEYS = [
|
|
|
56
73
|
* Non-workdays use CalendarDaySurfaceStyles (red).
|
|
57
74
|
* Pass `captureRef` only if capturing this live card; prefer {@link TimesheetExportCard} for PNG/PDF.
|
|
58
75
|
*/
|
|
59
|
-
export function Timesheet ({
|
|
76
|
+
export function Timesheet ({
|
|
77
|
+
lines = [],
|
|
78
|
+
onChange,
|
|
79
|
+
disabled = false,
|
|
80
|
+
periodStart,
|
|
81
|
+
captureRef,
|
|
82
|
+
employeeId,
|
|
83
|
+
contractId,
|
|
84
|
+
onEmployeeChange,
|
|
85
|
+
onContractChange,
|
|
86
|
+
}) {
|
|
60
87
|
const { t } = useLocale()
|
|
61
88
|
|
|
62
89
|
const lineByKey = useMemo(() => {
|
|
@@ -90,6 +117,11 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
|
|
|
90
117
|
<>
|
|
91
118
|
<style href="@ossy/timesheets/day-surface" precedence="high">
|
|
92
119
|
{`
|
|
120
|
+
[data-timesheet-root] [data-timesheet-day-number] {
|
|
121
|
+
color: var(--foreground);
|
|
122
|
+
font-family: var(--text-default-font-family, sans-serif);
|
|
123
|
+
}
|
|
124
|
+
|
|
93
125
|
[data-timesheet-root] [data-ossy-calendar-day] {
|
|
94
126
|
background: var(--surface-primary);
|
|
95
127
|
}
|
|
@@ -102,6 +134,40 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
|
|
|
102
134
|
[data-timesheet-root] [data-ossy-calendar-day][data-day-off="true"]:hover {
|
|
103
135
|
background: color-mix(in srgb, hsl(350 32% 55%) 13%, var(--surface-primary));
|
|
104
136
|
}
|
|
137
|
+
|
|
138
|
+
[data-timesheet-root] [data-ossy-calendar-day][data-day-off="true"] [data-timesheet-day-number] {
|
|
139
|
+
color: color-mix(in srgb, hsl(350 28% 38%) 72%, var(--foreground));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
[data-timesheet-root] [data-ossy-calendar-day]:focus-within {
|
|
143
|
+
background: color-mix(in srgb, var(--color-accent, #3dcf9a) 16%, var(--surface-primary));
|
|
144
|
+
box-shadow: inset 0 0 0 2px var(--color-accent, #3dcf9a);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
[data-timesheet-root] [data-ossy-calendar-day][data-day-off="true"]:focus-within {
|
|
148
|
+
background: color-mix(in srgb, var(--color-accent, #3dcf9a) 14%, color-mix(in srgb, hsl(350 32% 58%) 9%, var(--surface-primary)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
[data-timesheet-root] [data-ossy-calendar-day] [data-component="input"] {
|
|
152
|
+
font-weight: 700;
|
|
153
|
+
font-size: 14px;
|
|
154
|
+
text-align: center;
|
|
155
|
+
appearance: textfield;
|
|
156
|
+
-moz-appearance: textfield;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
[data-timesheet-root] [data-ossy-calendar-day] [data-component="input"]::-webkit-outer-spin-button,
|
|
160
|
+
[data-timesheet-root] [data-ossy-calendar-day] [data-component="input"]::-webkit-inner-spin-button {
|
|
161
|
+
-webkit-appearance: none;
|
|
162
|
+
margin: 0;
|
|
163
|
+
display: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
[data-timesheet-root] [data-ossy-calendar-day] [data-component="input"]:focus {
|
|
167
|
+
outline: none;
|
|
168
|
+
border-color: transparent;
|
|
169
|
+
box-shadow: none;
|
|
170
|
+
}
|
|
105
171
|
`}
|
|
106
172
|
</style>
|
|
107
173
|
<div
|
|
@@ -134,6 +200,19 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
|
|
|
134
200
|
/>
|
|
135
201
|
</View>
|
|
136
202
|
|
|
203
|
+
{typeof onEmployeeChange === 'function' && typeof onContractChange === 'function' ? (
|
|
204
|
+
<div style={{ width: 0, minWidth: '100%' }}>
|
|
205
|
+
<TimesheetPartyRows
|
|
206
|
+
employeeId={employeeId}
|
|
207
|
+
contractId={contractId}
|
|
208
|
+
onEmployeeChange={onEmployeeChange}
|
|
209
|
+
onContractChange={onContractChange}
|
|
210
|
+
disabled={disabled}
|
|
211
|
+
totalHours={totalHours}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
) : null}
|
|
215
|
+
|
|
137
216
|
<div style={calendarGrid}>
|
|
138
217
|
{WEEKDAY_KEYS.map((key) => (
|
|
139
218
|
<Text key={key} variant="small" color="secondary" style={{ textAlign: 'center' }}>
|
|
@@ -152,23 +231,20 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
|
|
|
152
231
|
const off = !line.isWorkday
|
|
153
232
|
|
|
154
233
|
return (
|
|
155
|
-
<
|
|
234
|
+
<label
|
|
156
235
|
key={cell.dateKey}
|
|
157
236
|
data-ossy-calendar-day
|
|
158
237
|
data-day-off={off}
|
|
159
238
|
style={dayCell}
|
|
160
239
|
>
|
|
161
|
-
<
|
|
240
|
+
<DateDisplay
|
|
241
|
+
value={line.date}
|
|
242
|
+
formatOptions={{ day: 'numeric' }}
|
|
162
243
|
variant="small"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
color: 'var(--foreground)',
|
|
168
|
-
}}
|
|
169
|
-
>
|
|
170
|
-
{cell.day}
|
|
171
|
-
</Text>
|
|
244
|
+
as="span"
|
|
245
|
+
data-timesheet-day-number
|
|
246
|
+
style={dateNumber}
|
|
247
|
+
/>
|
|
172
248
|
<Input
|
|
173
249
|
type="number"
|
|
174
250
|
min={0}
|
|
@@ -180,7 +256,7 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
|
|
|
180
256
|
style={hoursInput}
|
|
181
257
|
aria-label="timesheets.hours"
|
|
182
258
|
/>
|
|
183
|
-
</
|
|
259
|
+
</label>
|
|
184
260
|
)
|
|
185
261
|
})}
|
|
186
262
|
</div>
|
package/src/TimesheetCard.jsx
CHANGED
|
@@ -1,36 +1,75 @@
|
|
|
1
|
-
import React, { useMemo } from 'react'
|
|
2
|
-
import {
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
import { Button, ContextMenu, DateDisplay, Dropdown, Icon, List, Text, View, useLocale } from '@ossy/design-system'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Storage-style list row for a timesheet period.
|
|
6
6
|
*/
|
|
7
|
-
export function TimesheetCard ({ timesheet, onClick }) {
|
|
7
|
+
export function TimesheetCard ({ timesheet, onClick, selected = false, actions }) {
|
|
8
8
|
const { t } = useLocale()
|
|
9
|
+
const [menuOpen, setMenuOpen] = useState(false)
|
|
10
|
+
const [menuPosition, setMenuPosition] = useState(null)
|
|
9
11
|
const totalHours = useMemo(
|
|
10
12
|
() => (timesheet?.lines ?? []).reduce((sum, line) => sum + (Number(line.hours) || 0), 0),
|
|
11
13
|
[timesheet?.lines],
|
|
12
14
|
)
|
|
13
15
|
const status = timesheet?.status ?? 'draft'
|
|
14
16
|
|
|
17
|
+
const handleMenuOpenChange = useCallback((open) => {
|
|
18
|
+
setMenuOpen(open)
|
|
19
|
+
if (!open) setMenuPosition(null)
|
|
20
|
+
}, [])
|
|
21
|
+
|
|
22
|
+
const handleContextMenu = useCallback((event) => {
|
|
23
|
+
if (!actions) return
|
|
24
|
+
event.preventDefault()
|
|
25
|
+
event.stopPropagation()
|
|
26
|
+
setMenuPosition({ top: event.clientY, left: event.clientX })
|
|
27
|
+
setMenuOpen(true)
|
|
28
|
+
}, [actions])
|
|
29
|
+
|
|
15
30
|
return (
|
|
16
31
|
<List.Item
|
|
17
32
|
selectable
|
|
18
|
-
|
|
33
|
+
selected={selected}
|
|
19
34
|
onClick={onClick}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/>
|
|
28
|
-
<Text variant="small" color="secondary">
|
|
29
|
-
{t('timesheets.statusLabel', { status: t(`timesheets.status.${status}`) })}
|
|
30
|
-
{' · '}
|
|
31
|
-
{t('timesheets.totalHours', { hours: totalHours })}
|
|
35
|
+
onContextMenu={handleContextMenu}
|
|
36
|
+
leading={(
|
|
37
|
+
<Icon size="s" name="calendar" style={{ fill: 'hsl(0, 0%, 80%)' }} />
|
|
38
|
+
)}
|
|
39
|
+
meta={(
|
|
40
|
+
<Text variant="small">
|
|
41
|
+
{t('timesheets.meta.hoursValue', { hours: totalHours })} {t(`timesheets.status.${status}`)}
|
|
32
42
|
</Text>
|
|
33
|
-
|
|
43
|
+
)}
|
|
44
|
+
trailing={actions ? (
|
|
45
|
+
<Dropdown
|
|
46
|
+
trigger={(
|
|
47
|
+
<Button
|
|
48
|
+
prefix="more-vertical-alt"
|
|
49
|
+
variant="command"
|
|
50
|
+
aria-label={t('timesheets.rowActions')}
|
|
51
|
+
onClick={(event) => {
|
|
52
|
+
event.stopPropagation()
|
|
53
|
+
setMenuPosition(null)
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
open={menuOpen}
|
|
58
|
+
onOpenChange={handleMenuOpenChange}
|
|
59
|
+
position={menuPosition}
|
|
60
|
+
>
|
|
61
|
+
<View inset="xs" surface="primary" roundness="s">
|
|
62
|
+
<ContextMenu roundness="s" surface="primary">
|
|
63
|
+
{actions}
|
|
64
|
+
</ContextMenu>
|
|
65
|
+
</View>
|
|
66
|
+
</Dropdown>
|
|
67
|
+
) : undefined}
|
|
68
|
+
>
|
|
69
|
+
<DateDisplay
|
|
70
|
+
value={timesheet.periodStart}
|
|
71
|
+
formatOptions={{ month: 'long', year: 'numeric' }}
|
|
72
|
+
/>
|
|
34
73
|
</List.Item>
|
|
35
74
|
)
|
|
36
75
|
}
|
|
@@ -6,6 +6,8 @@ import { TIMESHEET_EXPORT_FONT_FAMILY } from './export-capture.js'
|
|
|
6
6
|
/** Matches on-screen Timesheet.jsx — solid colors for html-to-image capture. */
|
|
7
7
|
const FG = 'hsl(199, 90%, 10%)'
|
|
8
8
|
const FG_MUTED = 'hsl(182, 15%, 42%)'
|
|
9
|
+
const DATE_FG = 'hsl(182, 23%, 30%)'
|
|
10
|
+
const DATE_OFF_FG = 'hsl(350, 28%, 38%)'
|
|
9
11
|
const ACCENT = 'hsl(167, 89%, 43%)'
|
|
10
12
|
const CELL_BG = '#ffffff'
|
|
11
13
|
const OFF_BG = 'color-mix(in srgb, hsl(350 32% 58%) 9%, #ffffff)'
|
|
@@ -22,16 +24,15 @@ const calendarGrid = {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const dayCell = {
|
|
27
|
+
position: 'relative',
|
|
25
28
|
boxSizing: 'border-box',
|
|
26
29
|
display: 'flex',
|
|
27
|
-
flexDirection: 'column',
|
|
28
30
|
alignItems: 'center',
|
|
29
|
-
justifyContent: '
|
|
30
|
-
gap: 4,
|
|
31
|
+
justifyContent: 'center',
|
|
31
32
|
width: '100%',
|
|
32
33
|
minHeight: 72,
|
|
33
34
|
borderRadius: 2,
|
|
34
|
-
padding: '
|
|
35
|
+
padding: '18px 4px 8px',
|
|
35
36
|
background: CELL_BG,
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -45,12 +46,42 @@ const WEEKDAY_KEYS = [
|
|
|
45
46
|
'timesheets.weekday.sun',
|
|
46
47
|
]
|
|
47
48
|
|
|
49
|
+
function ExportMetaRow ({ label, value }) {
|
|
50
|
+
if (value == null || value === '') return null
|
|
51
|
+
return (
|
|
52
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
|
|
53
|
+
<span style={{ fontFamily: TIMESHEET_EXPORT_FONT_FAMILY, fontSize: 13, fontWeight: 400, color: FG_MUTED }}>
|
|
54
|
+
{label}
|
|
55
|
+
</span>
|
|
56
|
+
<span style={{ fontFamily: TIMESHEET_EXPORT_FONT_FAMILY, fontSize: 13, fontWeight: 400, color: FG }}>
|
|
57
|
+
{value}
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatSek (value, language) {
|
|
64
|
+
if (value == null || value === '' || !Number.isFinite(Number(value))) return ''
|
|
65
|
+
return new Intl.NumberFormat(language === 'sv' ? 'sv-SE' : 'en-SE', {
|
|
66
|
+
style: 'currency',
|
|
67
|
+
currency: 'SEK',
|
|
68
|
+
}).format(Number(value))
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
/**
|
|
49
72
|
* Print-ready timesheet (hours as text — no inputs).
|
|
50
73
|
* Matches on-screen {@link Timesheet} layout; white background, no card chrome.
|
|
51
74
|
* Mount off-screen and pass `captureRef` for PNG/PDF via html-to-image.
|
|
52
75
|
*/
|
|
53
|
-
export function TimesheetExportCard ({
|
|
76
|
+
export function TimesheetExportCard ({
|
|
77
|
+
lines = [],
|
|
78
|
+
periodStart,
|
|
79
|
+
captureRef,
|
|
80
|
+
employeeLabel,
|
|
81
|
+
contractLabel,
|
|
82
|
+
clientName,
|
|
83
|
+
hourlyRate,
|
|
84
|
+
}) {
|
|
54
85
|
const { t, language } = useLocale()
|
|
55
86
|
|
|
56
87
|
const lineByKey = useMemo(() => {
|
|
@@ -73,6 +104,12 @@ export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
|
|
|
73
104
|
.format(new Date(anchorMs))
|
|
74
105
|
}, [anchorMs, language])
|
|
75
106
|
|
|
107
|
+
const rate = hourlyRate == null || hourlyRate === '' ? null : Number(hourlyRate)
|
|
108
|
+
const totalCost = rate != null && Number.isFinite(rate) ? totalHours * rate : null
|
|
109
|
+
const hasPartyMeta = Boolean(
|
|
110
|
+
employeeLabel || contractLabel || clientName || (rate != null && Number.isFinite(rate)),
|
|
111
|
+
)
|
|
112
|
+
|
|
76
113
|
if (!lines.length) return null
|
|
77
114
|
|
|
78
115
|
return (
|
|
@@ -134,6 +171,25 @@ export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
|
|
|
134
171
|
width: '100%',
|
|
135
172
|
}}
|
|
136
173
|
/>
|
|
174
|
+
{hasPartyMeta ? (
|
|
175
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
176
|
+
<ExportMetaRow label={t('timesheets.employee')} value={employeeLabel} />
|
|
177
|
+
<ExportMetaRow label={t('timesheets.meta.client')} value={clientName} />
|
|
178
|
+
<ExportMetaRow label={t('timesheets.contract')} value={contractLabel} />
|
|
179
|
+
<ExportMetaRow
|
|
180
|
+
label={t('timesheets.meta.hourlyRate')}
|
|
181
|
+
value={
|
|
182
|
+
rate != null && Number.isFinite(rate)
|
|
183
|
+
? `${formatSek(rate, language)}${t('timesheets.meta.perHour')}`
|
|
184
|
+
: ''
|
|
185
|
+
}
|
|
186
|
+
/>
|
|
187
|
+
<ExportMetaRow
|
|
188
|
+
label={t('timesheets.meta.totalCost')}
|
|
189
|
+
value={totalCost != null && Number.isFinite(totalCost) ? formatSek(totalCost, language) : ''}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
) : null}
|
|
137
193
|
</div>
|
|
138
194
|
|
|
139
195
|
<div style={calendarGrid}>
|
|
@@ -175,12 +231,14 @@ export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
|
|
|
175
231
|
>
|
|
176
232
|
<span
|
|
177
233
|
style={{
|
|
178
|
-
|
|
234
|
+
position: 'absolute',
|
|
235
|
+
top: 4,
|
|
236
|
+
right: 5,
|
|
179
237
|
fontFamily: TIMESHEET_EXPORT_FONT_FAMILY,
|
|
180
|
-
fontSize:
|
|
181
|
-
fontWeight:
|
|
182
|
-
lineHeight: 1
|
|
183
|
-
color:
|
|
238
|
+
fontSize: 11,
|
|
239
|
+
fontWeight: 500,
|
|
240
|
+
lineHeight: 1,
|
|
241
|
+
color: off ? DATE_OFF_FG : DATE_FG,
|
|
184
242
|
}}
|
|
185
243
|
>
|
|
186
244
|
{cell.day}
|
|
@@ -190,14 +248,11 @@ export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
|
|
|
190
248
|
display: 'block',
|
|
191
249
|
boxSizing: 'border-box',
|
|
192
250
|
width: '100%',
|
|
193
|
-
maxWidth: 48,
|
|
194
|
-
minHeight: 32,
|
|
195
|
-
padding: '6px 4px',
|
|
196
251
|
textAlign: 'center',
|
|
197
252
|
fontFamily: TIMESHEET_EXPORT_FONT_FAMILY,
|
|
198
|
-
fontSize:
|
|
199
|
-
fontWeight:
|
|
200
|
-
lineHeight: 1
|
|
253
|
+
fontSize: 14,
|
|
254
|
+
fontWeight: 700,
|
|
255
|
+
lineHeight: 1,
|
|
201
256
|
color: 'inherit',
|
|
202
257
|
}}
|
|
203
258
|
>
|