@ossy/timesheets 3.8.0 → 3.10.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 +8 -3
- package/src/Timesheet.jsx +36 -1
- package/src/TimesheetCard.jsx +57 -18
- package/src/TimesheetExportCard.jsx +56 -1
- package/src/TimesheetPanel.jsx +249 -0
- package/src/TimesheetPartyFields.jsx +121 -0
- package/src/TimesheetPartyRows.jsx +101 -0
- package/src/TimesheetsProductHome.jsx +257 -69
- 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-timesheet-draft.js +81 -0
- 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 +23 -3
- package/src/generate.task.js +13 -71
- package/src/get.task.js +1 -1
- package/src/index.js +12 -1
- package/src/list.task.js +5 -1
- package/src/save.task.js +16 -5
- package/src/schema-ids.js +6 -0
- package/src/send-timesheet-reminder.task.js +76 -0
- package/src/sv.translations.json +23 -3
- package/src/timesheet-detail.page.jsx +10 -193
- package/src/timesheet-dto.js +26 -0
- package/src/timesheet-parties.js +57 -0
- package/src/timesheet-parties.spec.js +40 -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 -18
- package/src/timesheet-resources.spec.js +59 -0
- package/src/timesheet.schema.js +20 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import {
|
|
3
|
+
formatPeriodLabel,
|
|
4
|
+
resolveTimesheetReminderRecipient,
|
|
5
|
+
shouldNotifyTimesheetCreated,
|
|
6
|
+
timesheetAssignedUrl,
|
|
7
|
+
} from './timesheet-reminder.js'
|
|
8
|
+
|
|
9
|
+
describe('shouldNotifyTimesheetCreated', () => {
|
|
10
|
+
it('notifies only auto-generated creates', () => {
|
|
11
|
+
expect(shouldNotifyTimesheetCreated({
|
|
12
|
+
resourceId: 'sheet-1',
|
|
13
|
+
payload: { content: { autoGenerated: true } },
|
|
14
|
+
})).toBe(true)
|
|
15
|
+
|
|
16
|
+
expect(shouldNotifyTimesheetCreated({
|
|
17
|
+
resourceId: 'sheet-2',
|
|
18
|
+
payload: { content: { autoGenerated: false } },
|
|
19
|
+
})).toBe(false)
|
|
20
|
+
|
|
21
|
+
expect(shouldNotifyTimesheetCreated({
|
|
22
|
+
resourceId: 'sheet-3',
|
|
23
|
+
payload: { content: {} },
|
|
24
|
+
})).toBe(false)
|
|
25
|
+
|
|
26
|
+
expect(shouldNotifyTimesheetCreated({
|
|
27
|
+
payload: { content: { autoGenerated: true } },
|
|
28
|
+
})).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('resolveTimesheetReminderRecipient', () => {
|
|
33
|
+
it('requires a linked employee with a valid email', () => {
|
|
34
|
+
const employees = [
|
|
35
|
+
{ id: 'emp-1', content: { name: 'Ada', email: 'ada@example.com' } },
|
|
36
|
+
{ id: 'emp-2', content: { name: 'Bob', email: 'not-an-email' } },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
expect(resolveTimesheetReminderRecipient(employees, {
|
|
40
|
+
content: { employeeId: 'emp-1' },
|
|
41
|
+
})).toEqual({ email: 'ada@example.com', name: 'Ada' })
|
|
42
|
+
|
|
43
|
+
expect(resolveTimesheetReminderRecipient(employees, {
|
|
44
|
+
content: { employeeId: 'emp-2' },
|
|
45
|
+
})).toBe(null)
|
|
46
|
+
|
|
47
|
+
expect(resolveTimesheetReminderRecipient(employees, {
|
|
48
|
+
content: {},
|
|
49
|
+
})).toBe(null)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('timesheetAssignedUrl', () => {
|
|
54
|
+
it('builds the Swedish home URL with an optional sheet query', () => {
|
|
55
|
+
expect(timesheetAssignedUrl(undefined, 'https://app.ossy.se/')).toBe(
|
|
56
|
+
'https://app.ossy.se/tidrapporter',
|
|
57
|
+
)
|
|
58
|
+
expect(timesheetAssignedUrl('sheet-1', 'https://app.ossy.se')).toBe(
|
|
59
|
+
'https://app.ossy.se/tidrapporter?r=sheet-1',
|
|
60
|
+
)
|
|
61
|
+
expect(timesheetAssignedUrl('sheet-1', '')).toBe(null)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('formatPeriodLabel', () => {
|
|
66
|
+
it('formats inclusive local dates', () => {
|
|
67
|
+
const label = formatPeriodLabel(
|
|
68
|
+
new Date(2026, 7, 1).getTime(),
|
|
69
|
+
new Date(2026, 7, 31).getTime(),
|
|
70
|
+
'en-US',
|
|
71
|
+
)
|
|
72
|
+
expect(label).toContain('2026')
|
|
73
|
+
expect(label).toContain('–')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ResourcesQueries } from '@ossy/resources/server'
|
|
2
2
|
|
|
3
|
+
export {
|
|
4
|
+
optionalReference,
|
|
5
|
+
findTimesheetForPeriod,
|
|
6
|
+
toTimesheetDto,
|
|
7
|
+
} from './timesheet-dto.js'
|
|
8
|
+
|
|
3
9
|
/**
|
|
4
10
|
* List timesheet resources by schema id (and optional workspace).
|
|
5
11
|
*/
|
|
@@ -8,21 +14,3 @@ export function getTimesheetResources ({ schemaId, belongsTo } = {}) {
|
|
|
8
14
|
if (belongsTo) query.belongsTo = belongsTo
|
|
9
15
|
return ResourcesQueries.GetResources(query)
|
|
10
16
|
}
|
|
11
|
-
|
|
12
|
-
export function findTimesheetForPeriod (resources, periodStart) {
|
|
13
|
-
return (resources || []).find((r) => r.content?.periodStart === periodStart) ?? null
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function toTimesheetDto (resource) {
|
|
17
|
-
if (!resource) return null
|
|
18
|
-
return {
|
|
19
|
-
id: resource.id,
|
|
20
|
-
periodStart: resource.content?.periodStart,
|
|
21
|
-
periodEnd: resource.content?.periodEnd,
|
|
22
|
-
status: resource.content?.status ?? 'draft',
|
|
23
|
-
holidayLocale: resource.content?.holidayLocale ?? null,
|
|
24
|
-
lines: resource.content?.lines ?? [],
|
|
25
|
-
createdAt: resource.createdAt,
|
|
26
|
-
updatedAt: resource.updatedAt,
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import {
|
|
3
|
+
findTimesheetForPeriod,
|
|
4
|
+
optionalReference,
|
|
5
|
+
toTimesheetDto,
|
|
6
|
+
} from './timesheet-dto.js'
|
|
7
|
+
|
|
8
|
+
describe('optionalReference', () => {
|
|
9
|
+
it('normalizes ids, objects, and empty values', () => {
|
|
10
|
+
expect(optionalReference(' emp-1 ')).toEqual({ resourceId: 'emp-1' })
|
|
11
|
+
expect(optionalReference({ resourceId: 'emp-1' })).toEqual({ resourceId: 'emp-1' })
|
|
12
|
+
expect(optionalReference('')).toBe(null)
|
|
13
|
+
expect(optionalReference(null)).toBe(null)
|
|
14
|
+
expect(optionalReference({})).toBe(null)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('toTimesheetDto', () => {
|
|
19
|
+
it('includes optional employee and contract refs', () => {
|
|
20
|
+
expect(toTimesheetDto({
|
|
21
|
+
id: 'sheet-1',
|
|
22
|
+
content: {
|
|
23
|
+
periodStart: 1,
|
|
24
|
+
periodEnd: 2,
|
|
25
|
+
status: 'draft',
|
|
26
|
+
holidayLocale: 'SE',
|
|
27
|
+
employeeId: 'emp-1',
|
|
28
|
+
contractId: { resourceId: 'con-1' },
|
|
29
|
+
lines: [],
|
|
30
|
+
},
|
|
31
|
+
createdAt: 10,
|
|
32
|
+
updatedAt: 20,
|
|
33
|
+
})).toEqual({
|
|
34
|
+
id: 'sheet-1',
|
|
35
|
+
periodStart: 1,
|
|
36
|
+
periodEnd: 2,
|
|
37
|
+
status: 'draft',
|
|
38
|
+
holidayLocale: 'SE',
|
|
39
|
+
employeeId: { resourceId: 'emp-1' },
|
|
40
|
+
contractId: { resourceId: 'con-1' },
|
|
41
|
+
lines: [],
|
|
42
|
+
createdAt: 10,
|
|
43
|
+
updatedAt: 20,
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('findTimesheetForPeriod', () => {
|
|
49
|
+
it('returns the newest sheet when several share a period', () => {
|
|
50
|
+
const resources = [
|
|
51
|
+
{ id: 'old', createdAt: 1, content: { periodStart: 100 } },
|
|
52
|
+
{ id: 'new', createdAt: 3, content: { periodStart: 100 } },
|
|
53
|
+
{ id: 'mid', createdAt: 2, content: { periodStart: 100 } },
|
|
54
|
+
{ id: 'other', createdAt: 9, content: { periodStart: 200 } },
|
|
55
|
+
]
|
|
56
|
+
expect(findTimesheetForPeriod(resources, 100)?.id).toBe('new')
|
|
57
|
+
expect(findTimesheetForPeriod(resources, 999)).toBe(null)
|
|
58
|
+
})
|
|
59
|
+
})
|
package/src/timesheet.schema.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { TimesheetRefs, TimesheetsSchema } from './schema-ids.js'
|
|
2
|
+
|
|
1
3
|
export default {
|
|
2
4
|
name: 'Timesheet',
|
|
3
|
-
id:
|
|
5
|
+
id: TimesheetsSchema.timesheet,
|
|
4
6
|
categoryName: 'Timesheets',
|
|
5
7
|
icon: 'calendar',
|
|
6
8
|
fields: [
|
|
@@ -24,6 +26,23 @@ export default {
|
|
|
24
26
|
type: 'text',
|
|
25
27
|
description: 'ISO 3166-1 alpha-2 country used for workday/public-holiday prefill',
|
|
26
28
|
},
|
|
29
|
+
{
|
|
30
|
+
name: 'employeeId',
|
|
31
|
+
type: 'reference',
|
|
32
|
+
of: TimesheetRefs.employee,
|
|
33
|
+
description: 'Optional employee this timesheet belongs to',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'contractId',
|
|
37
|
+
type: 'reference',
|
|
38
|
+
of: TimesheetRefs.contract,
|
|
39
|
+
description: 'Optional contract this timesheet belongs to',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'autoGenerated',
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
description: 'Set when the daily auto-generate cron created this sheet',
|
|
45
|
+
},
|
|
27
46
|
// `lines` is stored in content (array of day rows) — same nested-content pattern as booking availability.
|
|
28
47
|
],
|
|
29
48
|
}
|