@ossy/timesheets 3.7.1 → 3.8.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 +3 -3
- package/src/HolidayLocaleSelect.jsx +32 -0
- package/src/Timesheet.jsx +61 -20
- package/src/TimesheetExportCard.jsx +15 -15
- package/src/TimesheetsProductHome.jsx +11 -1
- package/src/en.translations.json +2 -1
- package/src/holiday-locale.js +6 -64
- package/src/index.js +1 -1
- package/src/sv.translations.json +2 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.8.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"src": "./src"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@ossy/resources": "^3.
|
|
17
|
+
"@ossy/resources": "^3.8.0",
|
|
18
18
|
"html-to-image": "^1.11.13",
|
|
19
19
|
"nanoid": "^5.1.11",
|
|
20
20
|
"pdf-lib": "^1.17.1"
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"/src",
|
|
39
39
|
"README.md"
|
|
40
40
|
],
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "14548dfc2019e7258e8c75db527acbf66fe829bd"
|
|
42
42
|
}
|
|
@@ -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
|
@@ -13,25 +13,39 @@ const calendarGrid = {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const dayCell = {
|
|
16
|
+
position: 'relative',
|
|
16
17
|
boxSizing: 'border-box',
|
|
17
18
|
display: 'flex',
|
|
18
|
-
flexDirection: 'column',
|
|
19
19
|
alignItems: 'center',
|
|
20
|
-
justifyContent: '
|
|
21
|
-
gap: 4,
|
|
20
|
+
justifyContent: 'center',
|
|
22
21
|
width: '100%',
|
|
23
22
|
minHeight: 72,
|
|
23
|
+
margin: 0,
|
|
24
24
|
borderRadius: 'var(--space-xxs, 2px)',
|
|
25
|
-
padding: '
|
|
25
|
+
padding: '18px 4px 8px',
|
|
26
|
+
cursor: 'text',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const dateNumber = {
|
|
30
|
+
position: 'absolute',
|
|
31
|
+
top: 4,
|
|
32
|
+
right: 5,
|
|
33
|
+
fontSize: 11,
|
|
34
|
+
fontWeight: 500,
|
|
35
|
+
lineHeight: 1,
|
|
36
|
+
letterSpacing: 0,
|
|
37
|
+
fontFamily: 'var(--text-default-font-family, sans-serif)',
|
|
38
|
+
pointerEvents: 'none',
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
const hoursInput = {
|
|
29
42
|
width: '100%',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
padding: '6px 4px',
|
|
43
|
+
minHeight: 28,
|
|
44
|
+
padding: 0,
|
|
33
45
|
textAlign: 'center',
|
|
34
|
-
fontSize:
|
|
46
|
+
fontSize: 14,
|
|
47
|
+
fontWeight: 700,
|
|
48
|
+
lineHeight: 1,
|
|
35
49
|
borderRadius: 'var(--space-xxs, 2px)',
|
|
36
50
|
background: 'transparent',
|
|
37
51
|
borderColor: 'transparent',
|
|
@@ -90,6 +104,11 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
|
|
|
90
104
|
<>
|
|
91
105
|
<style href="@ossy/timesheets/day-surface" precedence="high">
|
|
92
106
|
{`
|
|
107
|
+
[data-timesheet-root] [data-timesheet-day-number] {
|
|
108
|
+
color: var(--foreground);
|
|
109
|
+
font-family: var(--text-default-font-family, sans-serif);
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
[data-timesheet-root] [data-ossy-calendar-day] {
|
|
94
113
|
background: var(--surface-primary);
|
|
95
114
|
}
|
|
@@ -102,6 +121,31 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
|
|
|
102
121
|
[data-timesheet-root] [data-ossy-calendar-day][data-day-off="true"]:hover {
|
|
103
122
|
background: color-mix(in srgb, hsl(350 32% 55%) 13%, var(--surface-primary));
|
|
104
123
|
}
|
|
124
|
+
|
|
125
|
+
[data-timesheet-root] [data-ossy-calendar-day][data-day-off="true"] [data-timesheet-day-number] {
|
|
126
|
+
color: color-mix(in srgb, hsl(350 28% 38%) 72%, var(--foreground));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
[data-timesheet-root] [data-ossy-calendar-day]:focus-within {
|
|
130
|
+
background: color-mix(in srgb, var(--color-accent, #3dcf9a) 16%, var(--surface-primary));
|
|
131
|
+
box-shadow: inset 0 0 0 2px var(--color-accent, #3dcf9a);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
[data-timesheet-root] [data-ossy-calendar-day][data-day-off="true"]:focus-within {
|
|
135
|
+
background: color-mix(in srgb, var(--color-accent, #3dcf9a) 14%, color-mix(in srgb, hsl(350 32% 58%) 9%, var(--surface-primary)));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[data-timesheet-root] [data-ossy-calendar-day] [data-component="input"] {
|
|
139
|
+
font-weight: 700;
|
|
140
|
+
font-size: 14px;
|
|
141
|
+
text-align: center;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[data-timesheet-root] [data-ossy-calendar-day] [data-component="input"]:focus {
|
|
145
|
+
outline: none;
|
|
146
|
+
border-color: transparent;
|
|
147
|
+
box-shadow: none;
|
|
148
|
+
}
|
|
105
149
|
`}
|
|
106
150
|
</style>
|
|
107
151
|
<div
|
|
@@ -152,23 +196,20 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
|
|
|
152
196
|
const off = !line.isWorkday
|
|
153
197
|
|
|
154
198
|
return (
|
|
155
|
-
<
|
|
199
|
+
<label
|
|
156
200
|
key={cell.dateKey}
|
|
157
201
|
data-ossy-calendar-day
|
|
158
202
|
data-day-off={off}
|
|
159
203
|
style={dayCell}
|
|
160
204
|
>
|
|
161
|
-
<
|
|
205
|
+
<DateDisplay
|
|
206
|
+
value={line.date}
|
|
207
|
+
formatOptions={{ day: 'numeric' }}
|
|
162
208
|
variant="small"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
color: 'var(--foreground)',
|
|
168
|
-
}}
|
|
169
|
-
>
|
|
170
|
-
{cell.day}
|
|
171
|
-
</Text>
|
|
209
|
+
as="span"
|
|
210
|
+
data-timesheet-day-number
|
|
211
|
+
style={dateNumber}
|
|
212
|
+
/>
|
|
172
213
|
<Input
|
|
173
214
|
type="number"
|
|
174
215
|
min={0}
|
|
@@ -180,7 +221,7 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
|
|
|
180
221
|
style={hoursInput}
|
|
181
222
|
aria-label="timesheets.hours"
|
|
182
223
|
/>
|
|
183
|
-
</
|
|
224
|
+
</label>
|
|
184
225
|
)
|
|
185
226
|
})}
|
|
186
227
|
</div>
|
|
@@ -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
|
|
|
@@ -175,12 +176,14 @@ export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
|
|
|
175
176
|
>
|
|
176
177
|
<span
|
|
177
178
|
style={{
|
|
178
|
-
|
|
179
|
+
position: 'absolute',
|
|
180
|
+
top: 4,
|
|
181
|
+
right: 5,
|
|
179
182
|
fontFamily: TIMESHEET_EXPORT_FONT_FAMILY,
|
|
180
|
-
fontSize:
|
|
181
|
-
fontWeight:
|
|
182
|
-
lineHeight: 1
|
|
183
|
-
color:
|
|
183
|
+
fontSize: 11,
|
|
184
|
+
fontWeight: 500,
|
|
185
|
+
lineHeight: 1,
|
|
186
|
+
color: off ? DATE_OFF_FG : DATE_FG,
|
|
184
187
|
}}
|
|
185
188
|
>
|
|
186
189
|
{cell.day}
|
|
@@ -190,14 +193,11 @@ export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
|
|
|
190
193
|
display: 'block',
|
|
191
194
|
boxSizing: 'border-box',
|
|
192
195
|
width: '100%',
|
|
193
|
-
maxWidth: 48,
|
|
194
|
-
minHeight: 32,
|
|
195
|
-
padding: '6px 4px',
|
|
196
196
|
textAlign: 'center',
|
|
197
197
|
fontFamily: TIMESHEET_EXPORT_FONT_FAMILY,
|
|
198
|
-
fontSize:
|
|
199
|
-
fontWeight:
|
|
200
|
-
lineHeight: 1
|
|
198
|
+
fontSize: 14,
|
|
199
|
+
fontWeight: 700,
|
|
200
|
+
lineHeight: 1,
|
|
201
201
|
color: 'inherit',
|
|
202
202
|
}}
|
|
203
203
|
>
|
|
@@ -9,6 +9,8 @@ import { invokeErrorMessage } from './invoke-error-message.js'
|
|
|
9
9
|
import { metadata as ListTimesheets } from './list.action.js'
|
|
10
10
|
import { metadata as GenerateTimesheet } from './generate.action.js'
|
|
11
11
|
import { metadata as OpenNewTimesheet } from './open-new.action.js'
|
|
12
|
+
import { HolidayLocaleSelect } from './HolidayLocaleSelect.jsx'
|
|
13
|
+
import { resolveHolidayLocale } from './holiday-locale.js'
|
|
12
14
|
|
|
13
15
|
function buildMonthOptions (language, yearsBack = 1, yearsForward = 0) {
|
|
14
16
|
let locale = 'en-GB'
|
|
@@ -46,6 +48,7 @@ export default function TimesheetsProductHome () {
|
|
|
46
48
|
const n = new Date()
|
|
47
49
|
return `${n.getFullYear()}-${n.getMonth()}`
|
|
48
50
|
})
|
|
51
|
+
const [holidayLocale, setHolidayLocale] = useState(() => resolveHolidayLocale(language))
|
|
49
52
|
const [creating, setCreating] = useState(false)
|
|
50
53
|
const [sheets, setSheets] = useState([])
|
|
51
54
|
const [loading, setLoading] = useState(true)
|
|
@@ -104,6 +107,7 @@ export default function TimesheetsProductHome () {
|
|
|
104
107
|
setCreateError(null)
|
|
105
108
|
const n = new Date()
|
|
106
109
|
setPeriodKey(`${n.getFullYear()}-${n.getMonth()}`)
|
|
110
|
+
setHolidayLocale(resolveHolidayLocale(language))
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
const handleGenerate = async () => {
|
|
@@ -114,7 +118,7 @@ export default function TimesheetsProductHome () {
|
|
|
114
118
|
const data = await sdk.invoke(GenerateTimesheet, {
|
|
115
119
|
year: selected.year,
|
|
116
120
|
monthIndex: selected.monthIndex,
|
|
117
|
-
locale:
|
|
121
|
+
locale: holidayLocale,
|
|
118
122
|
})
|
|
119
123
|
if (data?.id) {
|
|
120
124
|
openDetail(data.id)
|
|
@@ -196,6 +200,12 @@ export default function TimesheetsProductHome () {
|
|
|
196
200
|
</Select>
|
|
197
201
|
</View>
|
|
198
202
|
|
|
203
|
+
<HolidayLocaleSelect
|
|
204
|
+
value={holidayLocale}
|
|
205
|
+
disabled={generating}
|
|
206
|
+
onChange={(e) => setHolidayLocale(e.target.value)}
|
|
207
|
+
/>
|
|
208
|
+
|
|
199
209
|
{periodAlreadyExists && (
|
|
200
210
|
<Text variant="small" color="secondary" text="timesheets.periodExistsHint" />
|
|
201
211
|
)}
|
package/src/en.translations.json
CHANGED
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
"timesheets.detail.description": "Review and adjust hours, then save or export.",
|
|
33
33
|
"timesheets.new": "New",
|
|
34
34
|
"timesheets.createTitle": "New timesheet",
|
|
35
|
-
"timesheets.createDescription": "Choose a month to generate a prefilled timesheet.",
|
|
35
|
+
"timesheets.createDescription": "Choose a month and holiday calendar to generate a prefilled timesheet.",
|
|
36
36
|
"timesheets.period": "Period",
|
|
37
|
+
"timesheets.holidayLocale": "Holiday calendar",
|
|
37
38
|
"timesheets.generate": "Generate",
|
|
38
39
|
"timesheets.openExisting": "Open",
|
|
39
40
|
"timesheets.periodExistsHint": "A timesheet already exists for this period — Open loads it.",
|
package/src/holiday-locale.js
CHANGED
|
@@ -1,64 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** @type {Record<string, string>} */
|
|
10
|
-
const LANGUAGE_TO_COUNTRY = {
|
|
11
|
-
sv: 'SE',
|
|
12
|
-
en: 'GB',
|
|
13
|
-
nb: 'NO',
|
|
14
|
-
nn: 'NO',
|
|
15
|
-
no: 'NO',
|
|
16
|
-
da: 'DK',
|
|
17
|
-
fi: 'FI',
|
|
18
|
-
de: 'DE',
|
|
19
|
-
fr: 'FR',
|
|
20
|
-
nl: 'NL',
|
|
21
|
-
es: 'ES',
|
|
22
|
-
it: 'IT',
|
|
23
|
-
pt: 'PT',
|
|
24
|
-
pl: 'PL',
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const DEFAULT_HOLIDAY_LOCALE = 'SE'
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param {unknown} value
|
|
31
|
-
* @returns {string | null} Uppercase ISO country code, or null if invalid
|
|
32
|
-
*/
|
|
33
|
-
export function normalizeHolidayLocale (value) {
|
|
34
|
-
if (typeof value !== 'string') return null
|
|
35
|
-
const trimmed = value.trim()
|
|
36
|
-
if (!/^[A-Za-z]{2}$/.test(trimmed)) return null
|
|
37
|
-
return trimmed.toUpperCase()
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string | null | undefined} languageOrTag UI language or BCP 47 tag
|
|
42
|
-
* @param {string} [fallback]
|
|
43
|
-
* @returns {string} ISO country code for holiday calendars
|
|
44
|
-
*/
|
|
45
|
-
export function resolveHolidayLocale (languageOrTag, fallback = DEFAULT_HOLIDAY_LOCALE) {
|
|
46
|
-
if (typeof languageOrTag === 'string' && languageOrTag.trim()) {
|
|
47
|
-
try {
|
|
48
|
-
const loc = new Intl.Locale(languageOrTag.trim())
|
|
49
|
-
const region = normalizeHolidayLocale(loc.region)
|
|
50
|
-
if (region) return region
|
|
51
|
-
const fromLanguage = LANGUAGE_TO_COUNTRY[loc.language?.toLowerCase()]
|
|
52
|
-
if (fromLanguage) return fromLanguage
|
|
53
|
-
} catch {
|
|
54
|
-
// fall through
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const asCountry = normalizeHolidayLocale(languageOrTag)
|
|
58
|
-
if (asCountry) return asCountry
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return normalizeHolidayLocale(fallback) || DEFAULT_HOLIDAY_LOCALE
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export { DEFAULT_HOLIDAY_LOCALE }
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_HOLIDAY_LOCALE,
|
|
3
|
+
HOLIDAY_COUNTRY_CODES,
|
|
4
|
+
normalizeHolidayLocale,
|
|
5
|
+
resolveHolidayLocale,
|
|
6
|
+
} from '@ossy/calendar/holiday-locale'
|
package/src/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { downloadTimesheetCsv } from './download-timesheet-csv.js'
|
|
|
15
15
|
export { downloadTimesheetImage } from './download-timesheet-image.js'
|
|
16
16
|
export { downloadTimesheetPdf } from './download-timesheet-pdf.js'
|
|
17
17
|
export { buildPrefilledLines, monthPeriodBounds, WORKDAY_HOURS } from './build-prefilled-lines.js'
|
|
18
|
-
export { resolveHolidayLocale, DEFAULT_HOLIDAY_LOCALE } from './holiday-locale.js'
|
|
18
|
+
export { resolveHolidayLocale, DEFAULT_HOLIDAY_LOCALE, HOLIDAY_COUNTRY_CODES } from './holiday-locale.js'
|
|
19
19
|
export { Timesheet } from './Timesheet.jsx'
|
|
20
20
|
export { TimesheetExportCard } from './TimesheetExportCard.jsx'
|
|
21
21
|
export { TimesheetsFreeTool } from './TimesheetsFreeTool.jsx'
|
package/src/sv.translations.json
CHANGED
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
"timesheets.detail.description": "Granska och justera timmar, spara eller exportera.",
|
|
33
33
|
"timesheets.new": "Ny",
|
|
34
34
|
"timesheets.createTitle": "Ny tidrapport",
|
|
35
|
-
"timesheets.createDescription": "Välj en månad för att generera en förifylld tidrapport.",
|
|
35
|
+
"timesheets.createDescription": "Välj en månad och helgdagskalender för att generera en förifylld tidrapport.",
|
|
36
36
|
"timesheets.period": "Period",
|
|
37
|
+
"timesheets.holidayLocale": "Helgdagskalender",
|
|
37
38
|
"timesheets.generate": "Generera",
|
|
38
39
|
"timesheets.openExisting": "Öppna",
|
|
39
40
|
"timesheets.periodExistsHint": "Det finns redan en tidrapport för perioden — Öppna laddar den.",
|