@ossy/timesheets 3.10.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 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.10.0",
4
+ "version": "3.11.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "module": "./src/index.js",
@@ -17,8 +17,8 @@
17
17
  "src": "./src"
18
18
  },
19
19
  "dependencies": {
20
- "@ossy/email": "^3.0.9",
21
- "@ossy/resources": "^3.10.0",
20
+ "@ossy/email": "^3.11.0",
21
+ "@ossy/resources": "^3.11.0",
22
22
  "@ossy/schema": "^3.8.0",
23
23
  "html-to-image": "^1.11.13",
24
24
  "nanoid": "^5.1.11",
@@ -43,5 +43,5 @@
43
43
  "/src",
44
44
  "README.md"
45
45
  ],
46
- "gitHead": "198084eb98871876396ae625d043dd0f03aa0325"
46
+ "gitHead": "53ff8456920e09151c9d88df4a6c3ee958d811eb"
47
47
  }
@@ -33,6 +33,8 @@ export function TimesheetCard ({ timesheet, onClick, selected = false, actions }
33
33
  selected={selected}
34
34
  onClick={onClick}
35
35
  onContextMenu={handleContextMenu}
36
+ data-timesheet-id={timesheet?.id}
37
+ data-timesheet-status={status}
36
38
  leading={(
37
39
  <Icon size="s" name="calendar" style={{ fill: 'hsl(0, 0%, 80%)' }} />
38
40
  )}
@@ -11,11 +11,16 @@ import { useTimesheetParties } from './TimesheetPartyFields.jsx'
11
11
  import { contractBillingMeta, labelForContract, labelForEmployee } from './timesheet-parties.js'
12
12
  import { metadata as GetTimesheet } from './get.action.js'
13
13
  import { metadata as SaveTimesheet } from './save.action.js'
14
+ import { metadata as OpenDelete } from './open-delete.action.js'
15
+ import { metadata as OpenExport } from './open-export.action.js'
16
+ import { metadata as ExportCsv } from './export-csv.action.js'
17
+ import { metadata as ExportPng } from './export-png.action.js'
18
+ import { metadata as ExportPdf } from './export-pdf.action.js'
14
19
 
15
20
  /**
16
21
  * Storage-style side panel for reviewing and saving a timesheet.
17
22
  */
18
- export function TimesheetPanel ({ timesheetId, onClose, onSaved, width = '50%' }) {
23
+ export function TimesheetPanel ({ timesheetId, onClose, onSaved, onRequestDelete, width = '50%' }) {
19
24
  const { t } = useLocale()
20
25
  const sdk = useSdk()
21
26
  const captureRef = useRef(null)
@@ -130,6 +135,8 @@ export function TimesheetPanel ({ timesheetId, onClose, onSaved, width = '50%' }
130
135
  surface="primary"
131
136
  bordered
132
137
  roundness="xs"
138
+ data-timesheet-panel={timesheetId || undefined}
139
+ data-timesheet-panel-status={timesheet?.status || (loading ? 'loading' : undefined)}
133
140
  style={{ height: '100%', width, minHeight: 0, minWidth: 0, flexShrink: 0 }}
134
141
  >
135
142
  <View.Item>
@@ -151,6 +158,8 @@ export function TimesheetPanel ({ timesheetId, onClose, onSaved, width = '50%' }
151
158
  <Dropdown
152
159
  trigger={(
153
160
  <Button
161
+ {...OpenExport}
162
+ label={undefined}
154
163
  variant="command"
155
164
  prefix="software-download"
156
165
  disabled={exportDisabled}
@@ -160,25 +169,26 @@ export function TimesheetPanel ({ timesheetId, onClose, onSaved, width = '50%' }
160
169
  >
161
170
  <ContextMenu roundness="s" surface="primary">
162
171
  <ContextMenu.Item
172
+ {...ExportCsv}
163
173
  prefix="list"
164
- label="timesheets.exportCsv"
165
174
  onClick={() => runExport('csv')}
166
175
  />
167
176
  <ContextMenu.Item
177
+ {...ExportPng}
168
178
  prefix="image"
169
- label="timesheets.exportPng"
170
179
  onClick={() => runExport('png')}
171
180
  />
172
181
  <ContextMenu.Item
182
+ {...ExportPdf}
173
183
  prefix="file-document"
174
- label="timesheets.exportPdf"
175
184
  onClick={() => runExport('pdf')}
176
185
  />
177
186
  </ContextMenu>
178
187
  </Dropdown>
179
188
 
180
189
  <Button
181
- id={SaveTimesheet.id}
190
+ {...SaveTimesheet}
191
+ label={undefined}
182
192
  variant="command"
183
193
  prefix="check"
184
194
  disabled={!timesheet?.id || saving || loading}
@@ -186,6 +196,18 @@ export function TimesheetPanel ({ timesheetId, onClose, onSaved, width = '50%' }
186
196
  onClick={handleSave}
187
197
  />
188
198
 
199
+ {onRequestDelete && (
200
+ <Button
201
+ {...OpenDelete}
202
+ label={undefined}
203
+ variant="command-danger"
204
+ prefix="trash-empty"
205
+ disabled={!timesheet?.id || loading}
206
+ aria-label={t('timesheets.delete')}
207
+ onClick={() => onRequestDelete(timesheet)}
208
+ />
209
+ )}
210
+
189
211
  {onClose && (
190
212
  <Button
191
213
  prefix="close"
@@ -17,6 +17,10 @@ import { downloadTimesheetPdf } from './download-timesheet-pdf.js'
17
17
  import { invokeErrorMessage } from './invoke-error-message.js'
18
18
  import { sampleShowcaseTimesheet } from './sample-showcase-timesheet.js'
19
19
  import { timesheetsAuthHref } from './timesheets-auth-return.js'
20
+ import { metadata as OpenExport } from './open-export.action.js'
21
+ import { metadata as ExportCsv } from './export-csv.action.js'
22
+ import { metadata as ExportPng } from './export-png.action.js'
23
+ import { metadata as ExportPdf } from './export-pdf.action.js'
20
24
 
21
25
  /**
22
26
  * Client-only timesheet for the sales page — prefilled, editable, exportable.
@@ -230,29 +234,29 @@ export function TimesheetsFreeTool ({
230
234
  <Dropdown
231
235
  trigger={(
232
236
  <Button
237
+ {...OpenExport}
233
238
  variant="secondary"
234
239
  prefix="software-download"
235
240
  suffix="chevron-down"
236
241
  disabled={exportDisabled}
237
- >
238
- {exporting ? t('timesheets.exporting') : t('timesheets.export')}
239
- </Button>
242
+ label={exporting ? 'timesheets.exporting' : OpenExport.label}
243
+ />
240
244
  )}
241
245
  >
242
246
  <ContextMenu roundness="s" surface="primary">
243
247
  <ContextMenu.Item
248
+ {...ExportCsv}
244
249
  prefix="list"
245
- label="timesheets.exportCsv"
246
250
  onClick={() => runExport('csv')}
247
251
  />
248
252
  <ContextMenu.Item
253
+ {...ExportPng}
249
254
  prefix="image"
250
- label="timesheets.exportPng"
251
255
  onClick={() => runExport('png')}
252
256
  />
253
257
  <ContextMenu.Item
258
+ {...ExportPdf}
254
259
  prefix="file-document"
255
- label="timesheets.exportPdf"
256
260
  onClick={() => runExport('pdf')}
257
261
  />
258
262
  </ContextMenu>
@@ -16,6 +16,10 @@ import { metadata as ListTimesheets } from './list.action.js'
16
16
  import { metadata as GenerateTimesheet } from './generate.action.js'
17
17
  import { metadata as OpenNewTimesheet } from './open-new.action.js'
18
18
  import { metadata as DeleteTimesheet } from './delete.action.js'
19
+ import { metadata as OpenDelete } from './open-delete.action.js'
20
+ import { metadata as ExportCsv } from './export-csv.action.js'
21
+ import { metadata as ExportPng } from './export-png.action.js'
22
+ import { metadata as ExportPdf } from './export-pdf.action.js'
19
23
  import { HolidayLocaleSelect } from './HolidayLocaleSelect.jsx'
20
24
  import { resolveHolidayLocale } from './holiday-locale.js'
21
25
 
@@ -212,6 +216,7 @@ export default function TimesheetsProductHome () {
212
216
  <>
213
217
  <Page
214
218
  scroll
219
+ data-timesheets-status="on"
215
220
  title="timesheets.home.title"
216
221
  near={statusTags.length > 0 ? <Tags tags={statusTags} size="s" /> : undefined}
217
222
  description="timesheets.home.description"
@@ -232,7 +237,8 @@ export default function TimesheetsProductHome () {
232
237
  <Text as="h2" variant="small" weight="medium" text="timesheets.listTitle" />
233
238
  </View.Item>
234
239
  <Button
235
- id={OpenNewTimesheet.id}
240
+ {...OpenNewTimesheet}
241
+ label={undefined}
236
242
  variant="command"
237
243
  prefix="add"
238
244
  aria-label={t('timesheets.new')}
@@ -272,25 +278,26 @@ export default function TimesheetsProductHome () {
272
278
  />,
273
279
  <ContextMenu.Item
274
280
  key="csv"
281
+ {...ExportCsv}
275
282
  prefix="list"
276
- label="timesheets.exportCsv"
277
283
  onClick={() => requestExport(sheet, 'csv')}
278
284
  />,
279
285
  <ContextMenu.Item
280
286
  key="png"
287
+ {...ExportPng}
281
288
  prefix="image"
282
- label="timesheets.exportPng"
283
289
  onClick={() => requestExport(sheet, 'png')}
284
290
  />,
285
291
  <ContextMenu.Item
286
292
  key="pdf"
293
+ {...ExportPdf}
287
294
  prefix="file-document"
288
- label="timesheets.exportPdf"
289
295
  onClick={() => requestExport(sheet, 'pdf')}
290
296
  />,
291
297
  <ContextMenu.Separator key="separator" />,
292
298
  <ContextMenu.Item
293
299
  key="delete"
300
+ {...OpenDelete}
294
301
  variant="command-danger"
295
302
  prefix="trash-empty"
296
303
  label="timesheets.delete"
@@ -309,6 +316,7 @@ export default function TimesheetsProductHome () {
309
316
  timesheetId={selectedResourceId}
310
317
  onClose={clearSelectedResourceId}
311
318
  onSaved={refreshList}
319
+ onRequestDelete={setSheetToDelete}
312
320
  />
313
321
  </View>
314
322
  </Page>
@@ -328,7 +336,7 @@ export default function TimesheetsProductHome () {
328
336
  onClick: () => setSheetToDelete(null),
329
337
  },
330
338
  {
331
- label: 'timesheets.delete',
339
+ ...DeleteTimesheet,
332
340
  variant: 'command-danger',
333
341
  disabled: deleting,
334
342
  onClick: confirmDelete,
@@ -410,13 +418,12 @@ export default function TimesheetsProductHome () {
410
418
  onClick={closeCreateModal}
411
419
  />
412
420
  <Button
413
- id={GenerateTimesheet.id}
421
+ {...GenerateTimesheet}
422
+ label={generating ? 'timesheets.loading' : GenerateTimesheet.label}
414
423
  variant="cta"
415
424
  disabled={generating || !selected}
416
425
  onClick={handleGenerate}
417
- >
418
- {generating ? t('timesheets.loading') : t('timesheets.generate')}
419
- </Button>
426
+ />
420
427
  </View>
421
428
  </View>
422
429
  </View>
@@ -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,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
+ }
package/src/index.js CHANGED
@@ -11,6 +11,11 @@ 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'
@@ -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
+ }
@@ -20,11 +20,12 @@ export default function TimesheetsHomePage () {
20
20
  const app = useApp()
21
21
  const router = useRouter()
22
22
  const { workspace: shellWorkspace } = useShellWorkspace()
23
- const { read, invoke, invalidate } = useSdk()
23
+ const { read, invoke } = useSdk()
24
24
  const isAuthenticated = !!app?.isAuthenticated
25
25
  const {
26
26
  data: workspaceFromSdk,
27
27
  status: workspaceStatus,
28
+ refetch: refetchWorkspace,
28
29
  } = read(GetWorkspace, undefined, { enabled: isAuthenticated })
29
30
  const autoEnableStarted = useRef(false)
30
31
 
@@ -52,10 +53,12 @@ export default function TimesheetsHomePage () {
52
53
  && workspaceStatus !== AsyncStatus.Error
53
54
  && app?.workspaceServices == null
54
55
 
56
+ // Shared GetWorkspace cache with useShellWorkspace — refetch (not bare invalidate)
57
+ // awaits the post-enable read so home can switch sales → product reliably.
55
58
  const enableService = useCallback(() => {
56
59
  return invoke(EnableService, { service: TIMESHEETS_SERVICE })
57
- .then(() => invalidate(cacheKey(GetWorkspace)))
58
- }, [invoke, invalidate])
60
+ .then(() => refetchWorkspace())
61
+ }, [invoke, refetchWorkspace])
59
62
 
60
63
  useEffect(() => {
61
64
  if (!isAuthenticated || !enableRequested || autoEnableStarted.current) return