@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.
Files changed (36) hide show
  1. package/package.json +5 -3
  2. package/src/TimesheetCard.jsx +2 -0
  3. package/src/TimesheetPanel.jsx +27 -5
  4. package/src/TimesheetsFreeTool.jsx +10 -6
  5. package/src/TimesheetsProductHome.jsx +16 -9
  6. package/src/auto-generate-timesheets.js +70 -0
  7. package/src/auto-generate-timesheets.spec.js +90 -0
  8. package/src/auto-generate-timesheets.task.js +60 -0
  9. package/src/billing-period.js +123 -0
  10. package/src/billing-period.spec.js +89 -0
  11. package/src/build-prefilled-lines.js +3 -16
  12. package/src/create-and-save-timesheet.flow.js +34 -0
  13. package/src/create-timesheet-draft.js +81 -0
  14. package/src/delete-timesheet.flow.js +41 -0
  15. package/src/disable-timesheets.flow.js +58 -0
  16. package/src/enable-timesheets.flow.js +24 -0
  17. package/src/export-csv.action.js +7 -0
  18. package/src/export-pdf.action.js +7 -0
  19. package/src/export-png.action.js +7 -0
  20. package/src/export-timesheet-csv.flow.js +39 -0
  21. package/src/export-timesheet-pdf.flow.js +39 -0
  22. package/src/export-timesheet-png.flow.js +39 -0
  23. package/src/generate.task.js +8 -45
  24. package/src/index.js +14 -0
  25. package/src/open-delete.action.js +7 -0
  26. package/src/open-export.action.js +7 -0
  27. package/src/send-timesheet-reminder.task.js +76 -0
  28. package/src/timesheet-dto.js +26 -0
  29. package/src/timesheet-refs.js +16 -0
  30. package/src/timesheet-reminder.email.jsx +47 -0
  31. package/src/timesheet-reminder.js +62 -0
  32. package/src/timesheet-reminder.spec.js +75 -0
  33. package/src/timesheet-resources.js +6 -40
  34. package/src/timesheet-resources.spec.js +1 -1
  35. package/src/timesheet.schema.js +5 -0
  36. 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,7 @@
1
+ /** Client-only — downloads the open timesheet as CSV. */
2
+ export const metadata = {
3
+ id: '@ossy/timesheets/actions/export-csv',
4
+ access: 'workspace',
5
+ label: 'timesheets.exportCsv',
6
+ icon: 'list',
7
+ }
@@ -0,0 +1,7 @@
1
+ /** Client-only — downloads the open timesheet as PDF. */
2
+ export const metadata = {
3
+ id: '@ossy/timesheets/actions/export-pdf',
4
+ access: 'workspace',
5
+ label: 'timesheets.exportPdf',
6
+ icon: 'file-document',
7
+ }
@@ -0,0 +1,7 @@
1
+ /** Client-only — downloads the open timesheet as a PNG image. */
2
+ export const metadata = {
3
+ id: '@ossy/timesheets/actions/export-png',
4
+ access: 'workspace',
5
+ label: 'timesheets.exportPng',
6
+ icon: 'image',
7
+ }
@@ -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
+ }
@@ -1,10 +1,5 @@
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, 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
- periodStart = Number(periodStart)
41
- periodEnd = Number(periodEnd)
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
- status: 'draft',
61
- holidayLocale,
62
- lines,
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
- belongsTo: workspaceId,
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'
@@ -0,0 +1,7 @@
1
+ /** Client-only — opens the timesheet delete confirmation dialog. */
2
+ export const metadata = {
3
+ id: '@ossy/timesheets/actions/open-delete',
4
+ access: 'workspace',
5
+ label: 'timesheets.delete',
6
+ icon: 'trash-empty',
7
+ }
@@ -0,0 +1,7 @@
1
+ /** Client-only — opens the timesheet export menu. */
2
+ export const metadata = {
3
+ id: '@ossy/timesheets/actions/open-export',
4
+ access: 'workspace',
5
+ label: 'timesheets.export',
6
+ icon: 'software-download',
7
+ }