@ossy/timesheets 3.0.6 → 3.0.7
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 +20 -9
- package/src/Definition.js +2 -1
- package/src/HeroCover.jsx +113 -0
- package/src/SalesSection.jsx +76 -0
- package/src/Timesheet.jsx +181 -66
- package/src/TimesheetCard.jsx +36 -0
- package/src/TimesheetExportCard.jsx +212 -0
- package/src/TimesheetsFreeTool.jsx +319 -0
- package/src/TimesheetsProductHome.jsx +229 -0
- package/src/TimesheetsSalesPage.jsx +50 -0
- package/src/build-prefilled-lines.js +60 -0
- package/src/download-timesheet-csv.js +35 -0
- package/src/download-timesheet-image.js +15 -0
- package/src/download-timesheet-pdf.js +49 -0
- package/src/en.translations.json +69 -1
- package/src/export-capture.js +106 -0
- package/src/generate.action.js +5 -0
- package/src/generate.task.js +103 -0
- package/src/get.action.js +4 -0
- package/src/get.task.js +56 -0
- package/src/holiday-locale.js +64 -0
- package/src/index.js +22 -0
- package/src/invoke-error-message.js +10 -0
- package/src/list.action.js +4 -0
- package/src/list.task.js +37 -0
- package/src/locations.js +11 -0
- package/src/open-new.action.js +5 -0
- package/src/sample-showcase-timesheet.js +34 -0
- package/src/save.action.js +5 -0
- package/src/save.task.js +62 -0
- package/src/schema-ids.js +4 -0
- package/src/sv.translations.json +69 -1
- package/src/timesheet-detail.page.jsx +209 -0
- package/src/timesheet-resources.js +28 -0
- package/src/timesheet.schema.js +29 -0
- package/src/timesheets-auth-return.js +23 -0
- package/src/timesheets-home-content.js +54 -0
- package/src/timesheets.page.jsx +86 -27
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import { ResourcesEvents, commitResource, mutateResource } from '@ossy/resources/server'
|
|
3
|
+
import { TimesheetsSchema } from './schema-ids.js'
|
|
4
|
+
import { timesheets } from './locations.js'
|
|
5
|
+
import { buildPrefilledLines, monthPeriodBounds } from './build-prefilled-lines.js'
|
|
6
|
+
import { DEFAULT_HOLIDAY_LOCALE, resolveHolidayLocale } from './holiday-locale.js'
|
|
7
|
+
import {
|
|
8
|
+
findTimesheetForPeriod,
|
|
9
|
+
getTimesheetResources,
|
|
10
|
+
toTimesheetDto,
|
|
11
|
+
} from './timesheet-resources.js'
|
|
12
|
+
|
|
13
|
+
export const metadata = { id: '@ossy/timesheets/tasks/generate' }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate (or load) a timesheet for a calendar month.
|
|
17
|
+
*
|
|
18
|
+
* Payload: `{ year, monthIndex, locale? }` (monthIndex 0–11) OR `{ periodStart, periodEnd, locale? }`.
|
|
19
|
+
* `locale` is an ISO country code or BCP 47 tag used for workday/holiday prefill.
|
|
20
|
+
* If a saved sheet exists for the period, returns it without regenerating.
|
|
21
|
+
* If a draft exists, regenerates prefilled lines (keeps id).
|
|
22
|
+
*/
|
|
23
|
+
export async function run ({ payload, req, log }) {
|
|
24
|
+
const workspaceId = req?.workspaceId
|
|
25
|
+
|
|
26
|
+
if (!workspaceId) {
|
|
27
|
+
throw Object.assign(new Error('workspaceId is required'), { status: 400 })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { year, monthIndex, locale: payloadLocale } = payload ?? {}
|
|
31
|
+
let { periodStart, periodEnd } = payload ?? {}
|
|
32
|
+
|
|
33
|
+
if (periodStart == null || periodEnd == null) {
|
|
34
|
+
if (year == null || monthIndex == null) {
|
|
35
|
+
throw Object.assign(
|
|
36
|
+
new Error('year and monthIndex (or periodStart and periodEnd) are required'),
|
|
37
|
+
{ status: 400 },
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
;({ periodStart, periodEnd } = monthPeriodBounds(Number(year), Number(monthIndex)))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
periodStart = Number(periodStart)
|
|
44
|
+
periodEnd = Number(periodEnd)
|
|
45
|
+
|
|
46
|
+
if (!Number.isFinite(periodStart) || !Number.isFinite(periodEnd) || periodEnd < periodStart) {
|
|
47
|
+
throw Object.assign(new Error('invalid period'), { status: 400 })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const holidayLocale = resolveHolidayLocale(payloadLocale, DEFAULT_HOLIDAY_LOCALE)
|
|
51
|
+
|
|
52
|
+
log?.info(
|
|
53
|
+
`[timesheets/tasks/generate] period ${periodStart}–${periodEnd} for workspace ${workspaceId} (locale ${holidayLocale})`,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const resources = await getTimesheetResources({
|
|
57
|
+
schemaId: TimesheetsSchema.timesheet,
|
|
58
|
+
belongsTo: workspaceId,
|
|
59
|
+
})
|
|
60
|
+
const existing = findTimesheetForPeriod(resources, periodStart)
|
|
61
|
+
|
|
62
|
+
if (existing?.content?.status === 'saved') {
|
|
63
|
+
log?.info(`[timesheets/tasks/generate] Returning saved timesheet ${existing.id}`)
|
|
64
|
+
return toTimesheetDto(existing)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lines = buildPrefilledLines(periodStart, periodEnd, holidayLocale)
|
|
68
|
+
const content = {
|
|
69
|
+
periodStart,
|
|
70
|
+
periodEnd,
|
|
71
|
+
status: 'draft',
|
|
72
|
+
holidayLocale,
|
|
73
|
+
lines,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (existing) {
|
|
77
|
+
log?.info(`[timesheets/tasks/generate] Regenerating draft ${existing.id}`)
|
|
78
|
+
await mutateResource(
|
|
79
|
+
existing.id,
|
|
80
|
+
ResourcesEvents.Patched({
|
|
81
|
+
createdBy: req?.userId ?? 'system',
|
|
82
|
+
content,
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
return toTimesheetDto({ ...existing, content })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const resourceId = nanoid()
|
|
89
|
+
const event = ResourcesEvents.Created({
|
|
90
|
+
resourceId,
|
|
91
|
+
schemaId: TimesheetsSchema.timesheet,
|
|
92
|
+
createdBy: req?.userId ?? 'system',
|
|
93
|
+
belongsTo: workspaceId,
|
|
94
|
+
location: timesheets,
|
|
95
|
+
name: `timesheet-${periodStart}.json`,
|
|
96
|
+
content,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const created = await commitResource(event)
|
|
100
|
+
log?.info(`[timesheets/tasks/generate] Created timesheet ${resourceId}`)
|
|
101
|
+
|
|
102
|
+
return toTimesheetDto({ id: resourceId, content, ...created })
|
|
103
|
+
}
|
package/src/get.task.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { viewResource } from '@ossy/resources/server'
|
|
2
|
+
import { TimesheetsSchema } from './schema-ids.js'
|
|
3
|
+
import {
|
|
4
|
+
findTimesheetForPeriod,
|
|
5
|
+
getTimesheetResources,
|
|
6
|
+
toTimesheetDto,
|
|
7
|
+
} from './timesheet-resources.js'
|
|
8
|
+
|
|
9
|
+
export const metadata = { id: '@ossy/timesheets/tasks/get' }
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get a timesheet by `{ timesheetId }` or `{ periodStart }`.
|
|
13
|
+
*/
|
|
14
|
+
export async function run ({ payload, req, log }) {
|
|
15
|
+
const workspaceId = req?.workspaceId
|
|
16
|
+
|
|
17
|
+
if (!workspaceId) {
|
|
18
|
+
throw Object.assign(new Error('workspaceId is required'), { status: 400 })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { timesheetId, periodStart } = payload ?? {}
|
|
22
|
+
|
|
23
|
+
if (timesheetId) {
|
|
24
|
+
log?.info(`[timesheets/tasks/get] Loading timesheet ${timesheetId}`)
|
|
25
|
+
const resource = await viewResource(timesheetId)
|
|
26
|
+
|
|
27
|
+
if (!resource || resource.type !== TimesheetsSchema.timesheet) {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (resource.belongsTo && resource.belongsTo !== workspaceId) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return toTimesheetDto(resource)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (periodStart == null) {
|
|
39
|
+
throw Object.assign(
|
|
40
|
+
new Error('timesheetId or periodStart is required'),
|
|
41
|
+
{ status: 400 },
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const resources = await getTimesheetResources({
|
|
46
|
+
schemaId: TimesheetsSchema.timesheet,
|
|
47
|
+
belongsTo: workspaceId,
|
|
48
|
+
})
|
|
49
|
+
const existing = findTimesheetForPeriod(resources, Number(periodStart))
|
|
50
|
+
|
|
51
|
+
log?.info(
|
|
52
|
+
`[timesheets/tasks/get] period ${periodStart} → ${existing?.id ?? 'not found'}`,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return toTimesheetDto(existing)
|
|
56
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map UI language / BCP 47 tags to ISO 3166-1 alpha-2 country codes
|
|
3
|
+
* for `date-holidays` (CalendarClient.isWorkDay).
|
|
4
|
+
*
|
|
5
|
+
* Prefer an explicit region (`sv-SE` → `SE`, `en-US` → `US`).
|
|
6
|
+
* Language-only tags fall back to a common country for that language.
|
|
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 }
|
package/src/index.js
CHANGED
|
@@ -1 +1,23 @@
|
|
|
1
1
|
export { Definition } from './Definition.js'
|
|
2
|
+
export { TimesheetsSchema } from './schema-ids.js'
|
|
3
|
+
export {
|
|
4
|
+
location,
|
|
5
|
+
timesheets,
|
|
6
|
+
locationByType,
|
|
7
|
+
} from './locations.js'
|
|
8
|
+
export { metadata as GenerateTimesheet } from './generate.action.js'
|
|
9
|
+
export { metadata as SaveTimesheet } from './save.action.js'
|
|
10
|
+
export { metadata as ListTimesheets } from './list.action.js'
|
|
11
|
+
export { metadata as GetTimesheet } from './get.action.js'
|
|
12
|
+
export { metadata as OpenNewTimesheet } from './open-new.action.js'
|
|
13
|
+
export { invokeErrorMessage } from './invoke-error-message.js'
|
|
14
|
+
export { downloadTimesheetCsv } from './download-timesheet-csv.js'
|
|
15
|
+
export { downloadTimesheetImage } from './download-timesheet-image.js'
|
|
16
|
+
export { downloadTimesheetPdf } from './download-timesheet-pdf.js'
|
|
17
|
+
export { buildPrefilledLines, monthPeriodBounds, WORKDAY_HOURS } from './build-prefilled-lines.js'
|
|
18
|
+
export { resolveHolidayLocale, DEFAULT_HOLIDAY_LOCALE } from './holiday-locale.js'
|
|
19
|
+
export { Timesheet } from './Timesheet.jsx'
|
|
20
|
+
export { TimesheetExportCard } from './TimesheetExportCard.jsx'
|
|
21
|
+
export { TimesheetsFreeTool } from './TimesheetsFreeTool.jsx'
|
|
22
|
+
export { TIMESHEET_EXPORT_BASE_BG } from './export-capture.js'
|
|
23
|
+
export { TimesheetCard } from './TimesheetCard.jsx'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Normalize errors from `sdk.invoke` (Response, string, or Error). */
|
|
2
|
+
export async function invokeErrorMessage (err, fallback) {
|
|
3
|
+
if (typeof err === 'string') return err
|
|
4
|
+
if (err instanceof Response) {
|
|
5
|
+
const data = await err.json().catch(() => ({}))
|
|
6
|
+
return data?.message ?? data?.error ?? fallback
|
|
7
|
+
}
|
|
8
|
+
if (err instanceof Error) return err.message ?? fallback
|
|
9
|
+
return fallback
|
|
10
|
+
}
|
package/src/list.task.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { TimesheetsSchema } from './schema-ids.js'
|
|
2
|
+
import { getTimesheetResources, toTimesheetDto } from './timesheet-resources.js'
|
|
3
|
+
|
|
4
|
+
export const metadata = { id: '@ossy/timesheets/tasks/list' }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* List timesheets for the workspace. Optional `{ periodStart }` filter.
|
|
8
|
+
*/
|
|
9
|
+
export async function run ({ payload, req, log }) {
|
|
10
|
+
const workspaceId = req?.workspaceId
|
|
11
|
+
|
|
12
|
+
if (!workspaceId) {
|
|
13
|
+
throw Object.assign(new Error('workspaceId is required'), { status: 400 })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { periodStart } = payload ?? {}
|
|
17
|
+
|
|
18
|
+
log?.info(`[timesheets/tasks/list] Listing timesheets for workspace ${workspaceId}`)
|
|
19
|
+
|
|
20
|
+
let resources = await getTimesheetResources({
|
|
21
|
+
schemaId: TimesheetsSchema.timesheet,
|
|
22
|
+
belongsTo: workspaceId,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (periodStart != null) {
|
|
26
|
+
const ps = Number(periodStart)
|
|
27
|
+
resources = resources.filter((r) => r.content?.periodStart === ps)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sheets = resources
|
|
31
|
+
.map(toTimesheetDto)
|
|
32
|
+
.sort((a, b) => (b.periodStart ?? 0) - (a.periodStart ?? 0))
|
|
33
|
+
|
|
34
|
+
log?.info(`[timesheets/tasks/list] Found ${sheets.length} timesheet(s)`)
|
|
35
|
+
|
|
36
|
+
return sheets
|
|
37
|
+
}
|
package/src/locations.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TimesheetsSchema } from './schema-ids.js'
|
|
2
|
+
|
|
3
|
+
/** Timesheets feature root. */
|
|
4
|
+
export const location = '/@ossy/timesheets/'
|
|
5
|
+
|
|
6
|
+
/** Canonical location for timesheet documents. */
|
|
7
|
+
export const timesheets = '/@ossy/timesheets/sheets/'
|
|
8
|
+
|
|
9
|
+
export const locationByType = {
|
|
10
|
+
[TimesheetsSchema.timesheet]: timesheets,
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { buildPrefilledLines, monthPeriodBounds } from './build-prefilled-lines.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Demo timesheet for the sales hero — current month, locale-aware holidays,
|
|
5
|
+
* with a couple of workdays nudged so it looks reviewed.
|
|
6
|
+
*
|
|
7
|
+
* @param {string | null | undefined} language UI language / BCP 47 tag
|
|
8
|
+
* @returns {{ periodStart: number, periodEnd: number, lines: Array<{ date: number, hours: number, isWorkday: boolean }> }}
|
|
9
|
+
*/
|
|
10
|
+
export function sampleShowcaseTimesheet (language) {
|
|
11
|
+
const now = new Date()
|
|
12
|
+
const { periodStart, periodEnd } = monthPeriodBounds(now.getFullYear(), now.getMonth())
|
|
13
|
+
const lines = buildPrefilledLines(periodStart, periodEnd, language)
|
|
14
|
+
|
|
15
|
+
const workdayIndexes = []
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
if (lines[i].isWorkday) workdayIndexes.push(i)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Nudge mid-month workdays so the preview looks adjusted, not blank-perfect.
|
|
21
|
+
const tweakTargets = [
|
|
22
|
+
{ offset: Math.floor(workdayIndexes.length * 0.35), hours: 6 },
|
|
23
|
+
{ offset: Math.floor(workdayIndexes.length * 0.55), hours: 4 },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
for (const { offset, hours } of tweakTargets) {
|
|
27
|
+
const idx = workdayIndexes[offset]
|
|
28
|
+
if (idx != null) {
|
|
29
|
+
lines[idx] = { ...lines[idx], hours }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { periodStart, periodEnd, lines }
|
|
34
|
+
}
|
package/src/save.task.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ResourcesEvents, mutateResource, viewResource } from '@ossy/resources/server'
|
|
2
|
+
import { TimesheetsSchema } from './schema-ids.js'
|
|
3
|
+
import { toTimesheetDto } from './timesheet-resources.js'
|
|
4
|
+
|
|
5
|
+
export const metadata = { id: '@ossy/timesheets/tasks/save' }
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Persist lines and set status to `saved`.
|
|
9
|
+
* Payload: `{ timesheetId, lines }`
|
|
10
|
+
*/
|
|
11
|
+
export async function run ({ payload, req, log }) {
|
|
12
|
+
const workspaceId = req?.workspaceId
|
|
13
|
+
|
|
14
|
+
if (!workspaceId) {
|
|
15
|
+
throw Object.assign(new Error('workspaceId is required'), { status: 400 })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { timesheetId, lines } = payload ?? {}
|
|
19
|
+
|
|
20
|
+
if (!timesheetId) {
|
|
21
|
+
throw Object.assign(new Error('timesheetId is required'), { status: 400 })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!Array.isArray(lines)) {
|
|
25
|
+
throw Object.assign(new Error('lines must be an array'), { status: 400 })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const resource = await viewResource(timesheetId)
|
|
29
|
+
|
|
30
|
+
if (!resource || resource.type !== TimesheetsSchema.timesheet) {
|
|
31
|
+
throw Object.assign(new Error('timesheet not found'), { status: 404 })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (resource.belongsTo && resource.belongsTo !== workspaceId) {
|
|
35
|
+
throw Object.assign(new Error('timesheet not found'), { status: 404 })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const normalizedLines = lines.map((line) => ({
|
|
39
|
+
date: Number(line.date),
|
|
40
|
+
hours: Number(line.hours) || 0,
|
|
41
|
+
isWorkday: Boolean(line.isWorkday),
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
const content = {
|
|
45
|
+
periodStart: resource.content?.periodStart,
|
|
46
|
+
periodEnd: resource.content?.periodEnd,
|
|
47
|
+
status: 'saved',
|
|
48
|
+
lines: normalizedLines,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
log?.info(`[timesheets/tasks/save] Saving timesheet ${timesheetId}`)
|
|
52
|
+
|
|
53
|
+
await mutateResource(
|
|
54
|
+
timesheetId,
|
|
55
|
+
ResourcesEvents.Patched({
|
|
56
|
+
createdBy: req?.userId ?? 'system',
|
|
57
|
+
content,
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return toTimesheetDto({ ...resource, id: timesheetId, content })
|
|
62
|
+
}
|
package/src/sv.translations.json
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"timesheets/home.documentTitle": "Tidrapportering",
|
|
3
|
+
"timesheets/detail.documentTitle": "Tidrapport",
|
|
3
4
|
"timesheets.home.title": "Tidrapportering",
|
|
4
|
-
"timesheets.
|
|
5
|
+
"timesheets.nav.title": "Tidrapportering",
|
|
6
|
+
"timesheets.home.description": "Dina månatliga tidrapporter. Skapa en, granska timmar i kalendern, spara och exportera.",
|
|
7
|
+
"timesheets.catalog.pitch": "Testa gratis — justera och exportera månadstimmar; skapa konto för att spara",
|
|
8
|
+
"timesheets.home.cover.title": "Månadsvisa tidrapporter som är förifyllda",
|
|
9
|
+
"timesheets.home.cover.text": "Generera en månad med timmar, justera det som skiljer sig, spara och exportera som CSV, bild eller PDF.",
|
|
10
|
+
"timesheets.home.cover.ctaPrimary": "Kom igång gratis",
|
|
11
|
+
"timesheets.home.cover.ctaSecondary": "Testa gratis",
|
|
12
|
+
"timesheets.freeTool.sectionTitle": "Testa gratis",
|
|
13
|
+
"timesheets.freeTool.sectionDescription": "Justera den här månadens timmar och exportera som CSV, bild eller PDF. Inget sparas förrän du skapar konto.",
|
|
14
|
+
"timesheets.freeTool.saveCta": "Skapa konto för att spara",
|
|
15
|
+
"timesheets.freeTool.saveHint": "Ändringar stannar i den här webbläsaren tills du laddar om. Exportera när du vill — spara kräver konto.",
|
|
16
|
+
"timesheets.freeTool.saveHintAuthenticated": "Exportera när du vill. Aktivera tidrapportering i workspace för att spara månader permanent.",
|
|
17
|
+
"timesheets.freeTool.signInLink": "Har du redan konto? Logga in",
|
|
18
|
+
"timesheets.home.features.prefill.title": "Förifyll månaden",
|
|
19
|
+
"timesheets.home.features.prefill.text": "Arbetsdagar börjar på 8 timmar. Helger och röda dagar nollas och markeras — ingen tom kalkylarksmall.",
|
|
20
|
+
"timesheets.home.features.review.title": "Granska i kalender",
|
|
21
|
+
"timesheets.home.features.review.text": "Redigera timmar dag för dag på ett månadskort. Ändra bara det som skiljer sig från standard.",
|
|
22
|
+
"timesheets.home.features.export.title": "Exportera när du är klar",
|
|
23
|
+
"timesheets.home.features.export.text": "Ladda ner CSV för kalkylark, eller PNG och PDF som matchar kalenderkortet.",
|
|
24
|
+
"timesheets.home.features.calendar.title": "Kalenderanpassad",
|
|
25
|
+
"timesheets.home.features.calendar.text": "Arbetsdagar och helgdagar räknas ut utifrån din region — helger nollas som standard.",
|
|
26
|
+
"timesheets.sales.overview": "Översikt",
|
|
27
|
+
"timesheets.sales.features": "Funktioner",
|
|
28
|
+
"timesheets.sales.enable": "Aktivera tidrapportering",
|
|
29
|
+
"timesheets.sales.enableDescription": "Aktivera tidrapportering för ditt workspace så du kan generera, spara och exportera månatlig arbetstid.",
|
|
30
|
+
"timesheets.sales.enableError": "Kunde inte aktivera tidrapportering. Försök igen eller kontakta support.",
|
|
31
|
+
"timesheets.detail.title": "Tidrapport",
|
|
32
|
+
"timesheets.detail.description": "Granska och justera timmar, spara eller exportera.",
|
|
33
|
+
"timesheets.new": "Ny",
|
|
34
|
+
"timesheets.createTitle": "Ny tidrapport",
|
|
35
|
+
"timesheets.createDescription": "Välj en månad för att generera en förifylld tidrapport.",
|
|
36
|
+
"timesheets.period": "Period",
|
|
37
|
+
"timesheets.generate": "Generera",
|
|
38
|
+
"timesheets.openExisting": "Öppna",
|
|
39
|
+
"timesheets.periodExistsHint": "Det finns redan en tidrapport för perioden — Öppna laddar den.",
|
|
40
|
+
"timesheets.cancel": "Avbryt",
|
|
41
|
+
"timesheets.save": "Spara",
|
|
42
|
+
"timesheets.saving": "Sparar…",
|
|
43
|
+
"timesheets.loading": "Laddar…",
|
|
44
|
+
"timesheets.export": "Exportera",
|
|
45
|
+
"timesheets.exporting": "Exporterar…",
|
|
46
|
+
"timesheets.exportCsv": "CSV",
|
|
47
|
+
"timesheets.exportPng": "Bild (PNG)",
|
|
48
|
+
"timesheets.exportPdf": "PDF",
|
|
49
|
+
"timesheets.hours": "Timmar",
|
|
50
|
+
"timesheets.empty": "Inga dagar i den här tidrapporten.",
|
|
51
|
+
"timesheets.listTitle": "Dina tidrapporter",
|
|
52
|
+
"timesheets.listEmpty": "Inga tidrapporter ännu. Klicka på Ny för att generera en för en månad.",
|
|
53
|
+
"timesheets.open": "Öppna",
|
|
54
|
+
"timesheets.backToList": "← Alla tidrapporter",
|
|
55
|
+
"timesheets.saved": "Tidrapport sparad.",
|
|
56
|
+
"timesheets.unsaved": "Osparade ändringar",
|
|
57
|
+
"timesheets.statusLabel": "Status: {status}",
|
|
58
|
+
"timesheets.status.draft": "Utkast",
|
|
59
|
+
"timesheets.status.saved": "Sparad",
|
|
60
|
+
"timesheets.totalHours": "Totalt: {hours} h",
|
|
61
|
+
"timesheets.weekday.mon": "Mån",
|
|
62
|
+
"timesheets.weekday.tue": "Tis",
|
|
63
|
+
"timesheets.weekday.wed": "Ons",
|
|
64
|
+
"timesheets.weekday.thu": "Tor",
|
|
65
|
+
"timesheets.weekday.fri": "Fre",
|
|
66
|
+
"timesheets.weekday.sat": "Lör",
|
|
67
|
+
"timesheets.weekday.sun": "Sön",
|
|
68
|
+
"timesheets.errorLoad": "Kunde inte ladda tidrapporten.",
|
|
69
|
+
"timesheets.errorNotFound": "Tidrapporten hittades inte.",
|
|
70
|
+
"timesheets.errorGenerate": "Kunde inte generera tidrapporten.",
|
|
71
|
+
"timesheets.errorSave": "Kunde inte spara tidrapporten.",
|
|
72
|
+
"timesheets.errorExport": "Kunde inte exportera tidrapporten."
|
|
5
73
|
}
|