@ossy/timesheets 3.9.0 → 3.11.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 +5 -3
- package/src/TimesheetCard.jsx +2 -0
- package/src/TimesheetPanel.jsx +27 -5
- package/src/TimesheetsFreeTool.jsx +10 -6
- package/src/TimesheetsProductHome.jsx +16 -9
- package/src/auto-generate-timesheets.js +70 -0
- package/src/auto-generate-timesheets.spec.js +90 -0
- package/src/auto-generate-timesheets.task.js +60 -0
- package/src/billing-period.js +123 -0
- package/src/billing-period.spec.js +89 -0
- package/src/build-prefilled-lines.js +3 -16
- package/src/create-and-save-timesheet.flow.js +34 -0
- package/src/create-timesheet-draft.js +81 -0
- package/src/delete-timesheet.flow.js +41 -0
- package/src/disable-timesheets.flow.js +58 -0
- package/src/enable-timesheets.flow.js +24 -0
- package/src/export-csv.action.js +7 -0
- package/src/export-pdf.action.js +7 -0
- package/src/export-png.action.js +7 -0
- package/src/export-timesheet-csv.flow.js +39 -0
- package/src/export-timesheet-pdf.flow.js +39 -0
- package/src/export-timesheet-png.flow.js +39 -0
- package/src/generate.task.js +8 -45
- package/src/index.js +14 -0
- package/src/open-delete.action.js +7 -0
- package/src/open-export.action.js +7 -0
- package/src/send-timesheet-reminder.task.js +76 -0
- package/src/timesheet-dto.js +26 -0
- package/src/timesheet-refs.js +16 -0
- package/src/timesheet-reminder.email.jsx +47 -0
- package/src/timesheet-reminder.js +62 -0
- package/src/timesheet-reminder.spec.js +75 -0
- package/src/timesheet-resources.js +6 -40
- package/src/timesheet-resources.spec.js +1 -1
- package/src/timesheet.schema.js +5 -0
- package/src/timesheets.page.jsx +6 -3
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import {
|
|
3
|
+
findTimesheetForContractPeriod,
|
|
4
|
+
isContractActive,
|
|
5
|
+
normalizeBillingPeriod,
|
|
6
|
+
periodForContract,
|
|
7
|
+
weekPeriodBounds,
|
|
8
|
+
} from './billing-period.js'
|
|
9
|
+
|
|
10
|
+
describe('normalizeBillingPeriod', () => {
|
|
11
|
+
it('defaults unknown values to monthly', () => {
|
|
12
|
+
expect(normalizeBillingPeriod('weekly')).toBe('weekly')
|
|
13
|
+
expect(normalizeBillingPeriod('monthly')).toBe('monthly')
|
|
14
|
+
expect(normalizeBillingPeriod(undefined)).toBe('monthly')
|
|
15
|
+
expect(normalizeBillingPeriod('quarterly')).toBe('monthly')
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('weekPeriodBounds', () => {
|
|
20
|
+
it('returns Mon–Sun containing the reference day', () => {
|
|
21
|
+
// Wednesday 2026-08-19 local
|
|
22
|
+
const { periodStart, periodEnd } = weekPeriodBounds(new Date(2026, 7, 19))
|
|
23
|
+
expect(new Date(periodStart).getDay()).toBe(1) // Monday
|
|
24
|
+
expect(new Date(periodEnd).getDay()).toBe(0) // Sunday
|
|
25
|
+
expect(new Date(periodStart).getDate()).toBe(17)
|
|
26
|
+
expect(new Date(periodEnd).getDate()).toBe(23)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('treats Sunday as end of the current ISO week', () => {
|
|
30
|
+
const { periodStart, periodEnd } = weekPeriodBounds(new Date(2026, 7, 23))
|
|
31
|
+
expect(new Date(periodStart).getDate()).toBe(17)
|
|
32
|
+
expect(new Date(periodEnd).getDate()).toBe(23)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('periodForContract', () => {
|
|
37
|
+
it('uses calendar month when billingPeriod is monthly or missing', () => {
|
|
38
|
+
const asOf = new Date(2026, 7, 19)
|
|
39
|
+
expect(periodForContract({ billingPeriod: 'monthly' }, asOf)).toEqual({
|
|
40
|
+
periodStart: new Date(2026, 7, 1).setHours(0, 0, 0, 0),
|
|
41
|
+
periodEnd: new Date(2026, 7, 31).setHours(0, 0, 0, 0),
|
|
42
|
+
})
|
|
43
|
+
expect(periodForContract({}, asOf).periodStart).toBe(
|
|
44
|
+
new Date(2026, 7, 1).setHours(0, 0, 0, 0),
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('uses ISO week when billingPeriod is weekly', () => {
|
|
49
|
+
const asOf = new Date(2026, 7, 19)
|
|
50
|
+
expect(periodForContract({ billingPeriod: 'weekly' }, asOf)).toEqual(
|
|
51
|
+
weekPeriodBounds(asOf),
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('isContractActive', () => {
|
|
57
|
+
it('requires start ≤ asOf and end ≥ asOf when set', () => {
|
|
58
|
+
const asOf = new Date(2026, 7, 19)
|
|
59
|
+
expect(isContractActive({
|
|
60
|
+
startDate: new Date(2026, 0, 1).getTime(),
|
|
61
|
+
endDate: new Date(2026, 11, 31).getTime(),
|
|
62
|
+
}, asOf)).toBe(true)
|
|
63
|
+
|
|
64
|
+
expect(isContractActive({
|
|
65
|
+
startDate: new Date(2026, 8, 1).getTime(),
|
|
66
|
+
}, asOf)).toBe(false)
|
|
67
|
+
|
|
68
|
+
expect(isContractActive({
|
|
69
|
+
startDate: new Date(2026, 0, 1).getTime(),
|
|
70
|
+
endDate: new Date(2026, 6, 1).getTime(),
|
|
71
|
+
}, asOf)).toBe(false)
|
|
72
|
+
|
|
73
|
+
expect(isContractActive({
|
|
74
|
+
startDate: new Date(2026, 0, 1).getTime(),
|
|
75
|
+
}, asOf)).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('findTimesheetForContractPeriod', () => {
|
|
80
|
+
it('matches contract id + period and returns the newest', () => {
|
|
81
|
+
const resources = [
|
|
82
|
+
{ id: 'a', createdAt: 1, content: { periodStart: 100, contractId: 'c1' } },
|
|
83
|
+
{ id: 'b', createdAt: 3, content: { periodStart: 100, contractId: { resourceId: 'c1' } } },
|
|
84
|
+
{ id: 'c', createdAt: 9, content: { periodStart: 100, contractId: 'c2' } },
|
|
85
|
+
]
|
|
86
|
+
expect(findTimesheetForContractPeriod(resources, 'c1', 100)?.id).toBe('b')
|
|
87
|
+
expect(findTimesheetForContractPeriod(resources, 'c1', 999)).toBe(null)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
import { CalendarClient } from '@ossy/calendar'
|
|
2
2
|
import { DEFAULT_HOLIDAY_LOCALE, resolveHolidayLocale } from './holiday-locale.js'
|
|
3
|
+
import { monthPeriodBounds } from './billing-period.js'
|
|
4
|
+
|
|
5
|
+
export { monthPeriodBounds }
|
|
3
6
|
|
|
4
7
|
/** Prefill hours for a workday (SPEC). */
|
|
5
8
|
export const WORKDAY_HOURS = 8
|
|
6
9
|
|
|
7
|
-
/**
|
|
8
|
-
* Calendar-month period bounds as local midnights, returned as Unix ms.
|
|
9
|
-
* @param {number} year
|
|
10
|
-
* @param {number} monthIndex 0–11
|
|
11
|
-
*/
|
|
12
|
-
export function monthPeriodBounds (year, monthIndex) {
|
|
13
|
-
const start = new Date(year, monthIndex, 1)
|
|
14
|
-
const end = new Date(year, monthIndex + 1, 0)
|
|
15
|
-
start.setHours(0, 0, 0, 0)
|
|
16
|
-
end.setHours(0, 0, 0, 0)
|
|
17
|
-
return {
|
|
18
|
-
periodStart: start.getTime(),
|
|
19
|
-
periodEnd: end.getTime(),
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
10
|
/**
|
|
24
11
|
* Build prefilled timesheet lines for an inclusive period.
|
|
25
12
|
* Workdays → 8h; non-workdays → 0h. `isWorkday` is snapshotted.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import enableTimesheetsFlow from './enable-timesheets.flow.js'
|
|
2
|
+
import { metadata as OpenNewTimesheet } from './open-new.action.js'
|
|
3
|
+
import { metadata as GenerateTimesheet } from './generate.action.js'
|
|
4
|
+
import { metadata as SaveTimesheet } from './save.action.js'
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
id: '@ossy/timesheets/flows/create-and-save-timesheet',
|
|
8
|
+
feature: 'timesheets',
|
|
9
|
+
requires: ['server', 'database'],
|
|
10
|
+
timeout: 150_000,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* After enabling timesheets, open the create modal, generate a draft for the
|
|
15
|
+
* current month, and save it from the side panel.
|
|
16
|
+
*/
|
|
17
|
+
export default {
|
|
18
|
+
title: 'Create and save timesheet',
|
|
19
|
+
description:
|
|
20
|
+
'Signed-in user enables timesheets, generates a draft timesheet, saves it, and sees saved status in the panel and list',
|
|
21
|
+
steps: [
|
|
22
|
+
...enableTimesheetsFlow.steps,
|
|
23
|
+
{ result: { action: OpenNewTimesheet, timeout: 10000 } },
|
|
24
|
+
{ action: OpenNewTimesheet },
|
|
25
|
+
{ result: { action: GenerateTimesheet, timeout: 10000 } },
|
|
26
|
+
{ action: GenerateTimesheet },
|
|
27
|
+
{ result: { action: SaveTimesheet, timeout: 20000 } },
|
|
28
|
+
{ result: { selector: '[data-timesheet-panel-status="draft"]', timeout: 20000 } },
|
|
29
|
+
{ result: { selector: '[data-timesheet-status="draft"]', timeout: 20000 } },
|
|
30
|
+
{ action: SaveTimesheet },
|
|
31
|
+
{ result: { selector: '[data-timesheet-panel-status="saved"]', timeout: 20000 } },
|
|
32
|
+
{ result: { selector: '[data-timesheet-status="saved"]', timeout: 20000 } },
|
|
33
|
+
],
|
|
34
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import { ResourcesEvents, commitResource } from '@ossy/resources/server'
|
|
3
|
+
import { TimesheetsSchema } from './schema-ids.js'
|
|
4
|
+
import { timesheets } from './locations.js'
|
|
5
|
+
import { buildPrefilledLines } from './build-prefilled-lines.js'
|
|
6
|
+
import { DEFAULT_HOLIDAY_LOCALE, resolveHolidayLocale } from './holiday-locale.js'
|
|
7
|
+
import { toTimesheetDto } from './timesheet-dto.js'
|
|
8
|
+
import { optionalReference } from './timesheet-refs.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a draft timesheet resource (shared by manual generate and auto-generate).
|
|
12
|
+
*
|
|
13
|
+
* @param {object} options
|
|
14
|
+
* @param {string} options.workspaceId
|
|
15
|
+
* @param {number} options.periodStart
|
|
16
|
+
* @param {number} options.periodEnd
|
|
17
|
+
* @param {string} [options.locale]
|
|
18
|
+
* @param {unknown} [options.employeeId]
|
|
19
|
+
* @param {unknown} [options.contractId]
|
|
20
|
+
* @param {string} [options.createdBy]
|
|
21
|
+
* @param {boolean} [options.autoGenerated]
|
|
22
|
+
* @param {{ info?: Function }} [options.log]
|
|
23
|
+
*/
|
|
24
|
+
export async function createTimesheetDraft ({
|
|
25
|
+
workspaceId,
|
|
26
|
+
periodStart,
|
|
27
|
+
periodEnd,
|
|
28
|
+
locale,
|
|
29
|
+
employeeId: employeeIdRaw,
|
|
30
|
+
contractId: contractIdRaw,
|
|
31
|
+
createdBy = 'system',
|
|
32
|
+
autoGenerated = false,
|
|
33
|
+
log,
|
|
34
|
+
} = {}) {
|
|
35
|
+
if (!workspaceId) {
|
|
36
|
+
throw Object.assign(new Error('workspaceId is required'), { status: 400 })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
periodStart = Number(periodStart)
|
|
40
|
+
periodEnd = Number(periodEnd)
|
|
41
|
+
|
|
42
|
+
if (!Number.isFinite(periodStart) || !Number.isFinite(periodEnd) || periodEnd < periodStart) {
|
|
43
|
+
throw Object.assign(new Error('invalid period'), { status: 400 })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const holidayLocale = resolveHolidayLocale(locale, DEFAULT_HOLIDAY_LOCALE)
|
|
47
|
+
const employeeId = optionalReference(employeeIdRaw)
|
|
48
|
+
const contractId = optionalReference(contractIdRaw)
|
|
49
|
+
|
|
50
|
+
log?.info?.(
|
|
51
|
+
`[timesheets] create draft ${periodStart}–${periodEnd} for workspace ${workspaceId} (locale ${holidayLocale})`,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const lines = buildPrefilledLines(periodStart, periodEnd, holidayLocale)
|
|
55
|
+
const content = {
|
|
56
|
+
periodStart,
|
|
57
|
+
periodEnd,
|
|
58
|
+
status: 'draft',
|
|
59
|
+
holidayLocale,
|
|
60
|
+
lines,
|
|
61
|
+
...(employeeId ? { employeeId } : {}),
|
|
62
|
+
...(contractId ? { contractId } : {}),
|
|
63
|
+
...(autoGenerated ? { autoGenerated: true } : {}),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const resourceId = nanoid()
|
|
67
|
+
const event = ResourcesEvents.Created({
|
|
68
|
+
resourceId,
|
|
69
|
+
schemaId: TimesheetsSchema.timesheet,
|
|
70
|
+
createdBy,
|
|
71
|
+
belongsTo: workspaceId,
|
|
72
|
+
location: timesheets,
|
|
73
|
+
name: `timesheet-${periodStart}-${resourceId}.json`,
|
|
74
|
+
content,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const created = await commitResource(event)
|
|
78
|
+
log?.info?.(`[timesheets] Created timesheet ${resourceId}`)
|
|
79
|
+
|
|
80
|
+
return toTimesheetDto({ id: resourceId, content, ...created })
|
|
81
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import createAndSaveTimesheetFlow from './create-and-save-timesheet.flow.js'
|
|
2
|
+
import { metadata as OpenDelete } from './open-delete.action.js'
|
|
3
|
+
import { metadata as DeleteTimesheet } from './delete.action.js'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
id: '@ossy/timesheets/flows/delete-timesheet',
|
|
7
|
+
feature: 'timesheets',
|
|
8
|
+
requires: ['server', 'database'],
|
|
9
|
+
timeout: 150_000,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* After creating and saving a timesheet, open delete from the panel, confirm,
|
|
14
|
+
* and assert the saved row leaves the list.
|
|
15
|
+
*/
|
|
16
|
+
export default {
|
|
17
|
+
title: 'Delete timesheet',
|
|
18
|
+
description:
|
|
19
|
+
'Signed-in user creates and saves a timesheet, deletes it from the panel confirm dialog, and sees it leave the list',
|
|
20
|
+
steps: [
|
|
21
|
+
...createAndSaveTimesheetFlow.steps,
|
|
22
|
+
{ result: { action: OpenDelete, timeout: 10000 } },
|
|
23
|
+
{ action: OpenDelete },
|
|
24
|
+
{ result: { action: DeleteTimesheet, timeout: 10000 } },
|
|
25
|
+
{ action: DeleteTimesheet },
|
|
26
|
+
{
|
|
27
|
+
result: {
|
|
28
|
+
selector: '[data-timesheet-status="saved"]',
|
|
29
|
+
hidden: true,
|
|
30
|
+
timeout: 20000,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
result: {
|
|
35
|
+
selector: '[data-timesheet-panel]',
|
|
36
|
+
hidden: true,
|
|
37
|
+
timeout: 10000,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import enableTimesheetsFlow from './enable-timesheets.flow.js'
|
|
2
|
+
import { DisableService, EnableService } from '@ossy/workspaces'
|
|
3
|
+
|
|
4
|
+
const TIMESHEETS = '@ossy/timesheets'
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
id: '@ossy/timesheets/flows/disable-timesheets',
|
|
8
|
+
feature: 'timesheets',
|
|
9
|
+
requires: ['server', 'database'],
|
|
10
|
+
timeout: 120_000,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* After enabling timesheets, disable the package from the catalog detail
|
|
15
|
+
* toggle and confirm the timesheets home returns to the sales (off) state.
|
|
16
|
+
*/
|
|
17
|
+
export default {
|
|
18
|
+
title: 'Disable timesheets',
|
|
19
|
+
description:
|
|
20
|
+
'Signed-in user enables timesheets, disables it from the package catalog, and sees the sales home again',
|
|
21
|
+
steps: [
|
|
22
|
+
...enableTimesheetsFlow.steps,
|
|
23
|
+
{ page: '@packages/detail', params: { packageSlug: 'timesheets' } },
|
|
24
|
+
{
|
|
25
|
+
result: {
|
|
26
|
+
action: { ...DisableService, service: TIMESHEETS },
|
|
27
|
+
timeout: 15000,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
result: {
|
|
32
|
+
selector: `[data-package-service="${TIMESHEETS}"][data-service-enabled="true"]`,
|
|
33
|
+
timeout: 10000,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{ action: { ...DisableService, service: TIMESHEETS } },
|
|
37
|
+
{
|
|
38
|
+
result: {
|
|
39
|
+
action: { ...EnableService, service: TIMESHEETS },
|
|
40
|
+
timeout: 20000,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
result: {
|
|
45
|
+
selector: `[data-package-service="${TIMESHEETS}"][data-service-enabled="false"]`,
|
|
46
|
+
timeout: 10000,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{ page: '@timesheets/home' },
|
|
50
|
+
{ result: { selector: '[data-timesheets-status="off"]', timeout: 20000 } },
|
|
51
|
+
{
|
|
52
|
+
result: {
|
|
53
|
+
action: { ...EnableService, service: TIMESHEETS },
|
|
54
|
+
timeout: 10000,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import signUpFlow from '@ossy/authentication/sign-up.flow.js'
|
|
2
|
+
import { EnableService } from '@ossy/workspaces'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
id: '@ossy/timesheets/flows/enable-timesheets',
|
|
6
|
+
feature: 'timesheets',
|
|
7
|
+
requires: ['server', 'database'],
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* New workspace owner signs up and enables timesheets from the sales home.
|
|
12
|
+
*/
|
|
13
|
+
export default {
|
|
14
|
+
title: 'Enable timesheets',
|
|
15
|
+
description:
|
|
16
|
+
'New user registers, verifies email, and activates timesheets from the timesheets home sales page',
|
|
17
|
+
steps: [
|
|
18
|
+
...signUpFlow.steps,
|
|
19
|
+
{ page: '@timesheets/home' },
|
|
20
|
+
{ result: { action: { ...EnableService, service: '@ossy/timesheets' }, timeout: 15000 } },
|
|
21
|
+
{ action: { ...EnableService, service: '@ossy/timesheets' } },
|
|
22
|
+
{ result: { selector: '[data-timesheets-status="on"]', timeout: 20000 } },
|
|
23
|
+
],
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import createAndSaveTimesheetFlow from './create-and-save-timesheet.flow.js'
|
|
2
|
+
import { monthPeriodBounds } from './billing-period.js'
|
|
3
|
+
import { formatDateKey } from './build-prefilled-lines.js'
|
|
4
|
+
import { metadata as OpenExport } from './open-export.action.js'
|
|
5
|
+
import { metadata as ExportCsv } from './export-csv.action.js'
|
|
6
|
+
|
|
7
|
+
const now = new Date()
|
|
8
|
+
const { periodStart } = monthPeriodBounds(now.getFullYear(), now.getMonth())
|
|
9
|
+
const csvFilename = `timesheet-${formatDateKey(periodStart)}.csv`
|
|
10
|
+
|
|
11
|
+
export const metadata = {
|
|
12
|
+
id: '@ossy/timesheets/flows/export-timesheet-csv',
|
|
13
|
+
feature: 'timesheets',
|
|
14
|
+
requires: ['server', 'database'],
|
|
15
|
+
timeout: 150_000,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* After creating and saving a timesheet, open the panel export menu and
|
|
20
|
+
* download CSV (browser download event).
|
|
21
|
+
*/
|
|
22
|
+
export default {
|
|
23
|
+
title: 'Export timesheet CSV',
|
|
24
|
+
description:
|
|
25
|
+
'Signed-in user creates and saves a timesheet, opens export from the panel, and downloads CSV',
|
|
26
|
+
steps: [
|
|
27
|
+
...createAndSaveTimesheetFlow.steps,
|
|
28
|
+
{ result: { action: OpenExport, timeout: 10000 } },
|
|
29
|
+
{ action: OpenExport },
|
|
30
|
+
{ result: { action: ExportCsv, timeout: 10000 } },
|
|
31
|
+
{
|
|
32
|
+
download: {
|
|
33
|
+
action: ExportCsv,
|
|
34
|
+
filename: csvFilename,
|
|
35
|
+
timeout: 30000,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import createAndSaveTimesheetFlow from './create-and-save-timesheet.flow.js'
|
|
2
|
+
import { monthPeriodBounds } from './billing-period.js'
|
|
3
|
+
import { timesheetExportFilename } from './export-capture.js'
|
|
4
|
+
import { metadata as OpenExport } from './open-export.action.js'
|
|
5
|
+
import { metadata as ExportPdf } from './export-pdf.action.js'
|
|
6
|
+
|
|
7
|
+
const now = new Date()
|
|
8
|
+
const { periodStart } = monthPeriodBounds(now.getFullYear(), now.getMonth())
|
|
9
|
+
const pdfFilename = timesheetExportFilename(periodStart, 'pdf')
|
|
10
|
+
|
|
11
|
+
export const metadata = {
|
|
12
|
+
id: '@ossy/timesheets/flows/export-timesheet-pdf',
|
|
13
|
+
feature: 'timesheets',
|
|
14
|
+
requires: ['server', 'database'],
|
|
15
|
+
timeout: 150_000,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* After creating and saving a timesheet, open the panel export menu and
|
|
20
|
+
* download PDF (browser download event from html-to-image + pdf-lib).
|
|
21
|
+
*/
|
|
22
|
+
export default {
|
|
23
|
+
title: 'Export timesheet PDF',
|
|
24
|
+
description:
|
|
25
|
+
'Signed-in user creates and saves a timesheet, opens export from the panel, and downloads PDF',
|
|
26
|
+
steps: [
|
|
27
|
+
...createAndSaveTimesheetFlow.steps,
|
|
28
|
+
{ result: { action: OpenExport, timeout: 10000 } },
|
|
29
|
+
{ action: OpenExport },
|
|
30
|
+
{ result: { action: ExportPdf, timeout: 10000 } },
|
|
31
|
+
{
|
|
32
|
+
download: {
|
|
33
|
+
action: ExportPdf,
|
|
34
|
+
filename: pdfFilename,
|
|
35
|
+
timeout: 60000,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import createAndSaveTimesheetFlow from './create-and-save-timesheet.flow.js'
|
|
2
|
+
import { monthPeriodBounds } from './billing-period.js'
|
|
3
|
+
import { timesheetExportFilename } from './export-capture.js'
|
|
4
|
+
import { metadata as OpenExport } from './open-export.action.js'
|
|
5
|
+
import { metadata as ExportPng } from './export-png.action.js'
|
|
6
|
+
|
|
7
|
+
const now = new Date()
|
|
8
|
+
const { periodStart } = monthPeriodBounds(now.getFullYear(), now.getMonth())
|
|
9
|
+
const pngFilename = timesheetExportFilename(periodStart, 'png')
|
|
10
|
+
|
|
11
|
+
export const metadata = {
|
|
12
|
+
id: '@ossy/timesheets/flows/export-timesheet-png',
|
|
13
|
+
feature: 'timesheets',
|
|
14
|
+
requires: ['server', 'database'],
|
|
15
|
+
timeout: 150_000,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* After creating and saving a timesheet, open the panel export menu and
|
|
20
|
+
* download PNG (browser download event from html-to-image capture).
|
|
21
|
+
*/
|
|
22
|
+
export default {
|
|
23
|
+
title: 'Export timesheet PNG',
|
|
24
|
+
description:
|
|
25
|
+
'Signed-in user creates and saves a timesheet, opens export from the panel, and downloads PNG',
|
|
26
|
+
steps: [
|
|
27
|
+
...createAndSaveTimesheetFlow.steps,
|
|
28
|
+
{ result: { action: OpenExport, timeout: 10000 } },
|
|
29
|
+
{ action: OpenExport },
|
|
30
|
+
{ result: { action: ExportPng, timeout: 10000 } },
|
|
31
|
+
{
|
|
32
|
+
download: {
|
|
33
|
+
action: ExportPng,
|
|
34
|
+
filename: pngFilename,
|
|
35
|
+
timeout: 60000,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}
|
package/src/generate.task.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 { toTimesheetDto, optionalReference } from './timesheet-resources.js'
|
|
1
|
+
import { monthPeriodBounds } from './billing-period.js'
|
|
2
|
+
import { createTimesheetDraft } from './create-timesheet-draft.js'
|
|
8
3
|
|
|
9
4
|
export const metadata = { id: '@ossy/timesheets/tasks/generate' }
|
|
10
5
|
|
|
@@ -37,46 +32,14 @@ export async function run ({ payload, req, log }) {
|
|
|
37
32
|
;({ periodStart, periodEnd } = monthPeriodBounds(Number(year), Number(monthIndex)))
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!Number.isFinite(periodStart) || !Number.isFinite(periodEnd) || periodEnd < periodStart) {
|
|
44
|
-
throw Object.assign(new Error('invalid period'), { status: 400 })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const holidayLocale = resolveHolidayLocale(payloadLocale, DEFAULT_HOLIDAY_LOCALE)
|
|
48
|
-
|
|
49
|
-
log?.info(
|
|
50
|
-
`[timesheets/tasks/generate] period ${periodStart}–${periodEnd} for workspace ${workspaceId} (locale ${holidayLocale})`,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
const employeeId = optionalReference(payload?.employeeId)
|
|
54
|
-
const contractId = optionalReference(payload?.contractId)
|
|
55
|
-
|
|
56
|
-
const lines = buildPrefilledLines(periodStart, periodEnd, holidayLocale)
|
|
57
|
-
const content = {
|
|
35
|
+
return createTimesheetDraft({
|
|
36
|
+
workspaceId,
|
|
58
37
|
periodStart,
|
|
59
38
|
periodEnd,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
...(employeeId ? { employeeId } : {}),
|
|
64
|
-
...(contractId ? { contractId } : {}),
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const resourceId = nanoid()
|
|
68
|
-
const event = ResourcesEvents.Created({
|
|
69
|
-
resourceId,
|
|
70
|
-
schemaId: TimesheetsSchema.timesheet,
|
|
39
|
+
locale: payloadLocale,
|
|
40
|
+
employeeId: payload?.employeeId,
|
|
41
|
+
contractId: payload?.contractId,
|
|
71
42
|
createdBy: req?.userId ?? 'system',
|
|
72
|
-
|
|
73
|
-
location: timesheets,
|
|
74
|
-
name: `timesheet-${periodStart}-${resourceId}.json`,
|
|
75
|
-
content,
|
|
43
|
+
log,
|
|
76
44
|
})
|
|
77
|
-
|
|
78
|
-
const created = await commitResource(event)
|
|
79
|
-
log?.info(`[timesheets/tasks/generate] Created timesheet ${resourceId}`)
|
|
80
|
-
|
|
81
|
-
return toTimesheetDto({ id: resourceId, content, ...created })
|
|
82
45
|
}
|
package/src/index.js
CHANGED
|
@@ -11,11 +11,25 @@ export { metadata as ListTimesheets } from './list.action.js'
|
|
|
11
11
|
export { metadata as GetTimesheet } from './get.action.js'
|
|
12
12
|
export { metadata as DeleteTimesheet } from './delete.action.js'
|
|
13
13
|
export { metadata as OpenNewTimesheet } from './open-new.action.js'
|
|
14
|
+
export { metadata as OpenDeleteTimesheet } from './open-delete.action.js'
|
|
15
|
+
export { metadata as OpenExportTimesheet } from './open-export.action.js'
|
|
16
|
+
export { metadata as ExportTimesheetCsv } from './export-csv.action.js'
|
|
17
|
+
export { metadata as ExportTimesheetPng } from './export-png.action.js'
|
|
18
|
+
export { metadata as ExportTimesheetPdf } from './export-pdf.action.js'
|
|
14
19
|
export { invokeErrorMessage } from './invoke-error-message.js'
|
|
15
20
|
export { downloadTimesheetCsv } from './download-timesheet-csv.js'
|
|
16
21
|
export { downloadTimesheetImage } from './download-timesheet-image.js'
|
|
17
22
|
export { downloadTimesheetPdf } from './download-timesheet-pdf.js'
|
|
18
23
|
export { buildPrefilledLines, monthPeriodBounds, WORKDAY_HOURS } from './build-prefilled-lines.js'
|
|
24
|
+
export {
|
|
25
|
+
BILLING_PERIODS,
|
|
26
|
+
DEFAULT_BILLING_PERIOD,
|
|
27
|
+
findTimesheetForContractPeriod,
|
|
28
|
+
isContractActive,
|
|
29
|
+
normalizeBillingPeriod,
|
|
30
|
+
periodForContract,
|
|
31
|
+
weekPeriodBounds,
|
|
32
|
+
} from './billing-period.js'
|
|
19
33
|
export { resolveHolidayLocale, DEFAULT_HOLIDAY_LOCALE, HOLIDAY_COUNTRY_CODES } from './holiday-locale.js'
|
|
20
34
|
export { Timesheet } from './Timesheet.jsx'
|
|
21
35
|
export { TimesheetExportCard } from './TimesheetExportCard.jsx'
|