@happyvertical/smrt-timesheets 0.53.1 → 0.53.2

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 (46) hide show
  1. package/AGENTS.md +32 -21
  2. package/README.md +124 -58
  3. package/agents/period-rollup.md +130 -0
  4. package/dist/attendance/models.d.ts +59 -0
  5. package/dist/attendance/models.d.ts.map +1 -0
  6. package/dist/attendance/service.d.ts +64 -0
  7. package/dist/attendance/service.d.ts.map +1 -0
  8. package/dist/attendance.d.ts +5 -0
  9. package/dist/attendance.d.ts.map +1 -0
  10. package/dist/attendance.js +323 -0
  11. package/dist/attendance.js.map +1 -0
  12. package/dist/chunks/attendance-Bk8o96dt.js +60 -0
  13. package/dist/chunks/attendance-Bk8o96dt.js.map +1 -0
  14. package/dist/chunks/models-D9n8HKXH.js +166 -0
  15. package/dist/chunks/models-D9n8HKXH.js.map +1 -0
  16. package/dist/chunks/rollup-yE0TFLJe.js +425 -0
  17. package/dist/chunks/rollup-yE0TFLJe.js.map +1 -0
  18. package/dist/chunks/service-time-entry-B2gHrJYI.js +190 -0
  19. package/dist/chunks/service-time-entry-B2gHrJYI.js.map +1 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +25 -205
  23. package/dist/index.js.map +1 -1
  24. package/dist/manifest.json +2553 -652
  25. package/dist/rollup/attendance.d.ts +8 -0
  26. package/dist/rollup/attendance.d.ts.map +1 -0
  27. package/dist/rollup/calculation.d.ts +7 -0
  28. package/dist/rollup/calculation.d.ts.map +1 -0
  29. package/dist/rollup/models.d.ts +47 -0
  30. package/dist/rollup/models.d.ts.map +1 -0
  31. package/dist/rollup/service.d.ts +37 -0
  32. package/dist/rollup/service.d.ts.map +1 -0
  33. package/dist/rollup/types.d.ts +38 -0
  34. package/dist/rollup/types.d.ts.map +1 -0
  35. package/dist/rollup/write.d.ts +4 -0
  36. package/dist/rollup/write.d.ts.map +1 -0
  37. package/dist/rollup.d.ts +5 -0
  38. package/dist/rollup.d.ts.map +1 -0
  39. package/dist/rollup.js +2 -0
  40. package/dist/smrt-knowledge.json +1319 -433
  41. package/package.json +19 -10
  42. package/dist/svelte/components/__tests__/DurationDisplay.test.js +0 -57
  43. package/dist/svelte/components/__tests__/TimeEntryApprovalQueue.test.js +0 -114
  44. package/dist/svelte/components/__tests__/TimeEntryCard.test.js +0 -45
  45. package/dist/svelte/components/__tests__/TimeEntryList.test.js +0 -50
  46. package/dist/svelte/components/__tests__/TimeSummary.test.js +0 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-timesheets",
3
- "version": "0.53.1",
3
+ "version": "0.53.2",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Shared time entries for the SMRT framework: who worked how long on what, approval and correction, with immutable charge and compensation snapshots",
6
6
  "type": "module",
@@ -10,7 +10,8 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "CLAUDE.md",
13
- "AGENTS.md"
13
+ "AGENTS.md",
14
+ "agents"
14
15
  ],
15
16
  "exports": {
16
17
  ".": {
@@ -27,13 +28,22 @@
27
28
  "import": "./dist/svelte/index.js"
28
29
  },
29
30
  "./manifest": "./dist/manifest.json",
30
- "./manifest.json": "./dist/manifest.json"
31
+ "./manifest.json": "./dist/manifest.json",
32
+ "./attendance": {
33
+ "types": "./dist/attendance.d.ts",
34
+ "import": "./dist/attendance.js"
35
+ },
36
+ "./rollup": {
37
+ "types": "./dist/rollup.d.ts",
38
+ "import": "./dist/rollup.js"
39
+ }
31
40
  },
32
41
  "dependencies": {
33
- "@happyvertical/smrt-core": "0.53.1",
34
- "@happyvertical/smrt-tenancy": "0.53.1",
35
- "@happyvertical/smrt-types": "0.53.1",
36
- "@happyvertical/smrt-ui": "0.53.1"
42
+ "@happyvertical/smrt-core": "0.53.2",
43
+ "@happyvertical/smrt-tenancy": "0.53.2",
44
+ "@happyvertical/smrt-types": "0.53.2",
45
+ "@happyvertical/smrt-ui": "0.53.2",
46
+ "@happyvertical/sql": "^0.98.0"
37
47
  },
38
48
  "peerDependencies": {
39
49
  "svelte": "^5.57.0"
@@ -44,8 +54,7 @@
44
54
  }
45
55
  },
46
56
  "devDependencies": {
47
- "@happyvertical/smrt-vitest": "0.53.1",
48
- "@happyvertical/sql": "^0.98.0",
57
+ "@happyvertical/smrt-vitest": "0.53.2",
49
58
  "@sveltejs/package": "^2.5.8",
50
59
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
51
60
  "@types/node": "24.13.6",
@@ -75,7 +84,7 @@
75
84
  "directory": "packages/timesheets"
76
85
  },
77
86
  "scripts": {
78
- "build": "vite build --mode library && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json",
87
+ "build": "vite build --mode library && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json && node ../../scripts/prune-svelte-package-artifacts.js dist",
79
88
  "build:watch": "vite build --mode library --watch",
80
89
  "clean": "rm -rf dist",
81
90
  "dev": "vite dev",
@@ -1,57 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * First component test in smrt-projects via the shared S11 harness (#1416).
4
- */
5
- import { expectNoA11yViolations, render, screen, } from '@happyvertical/smrt-vitest/svelte';
6
- import { describe, expect, it } from 'vitest';
7
- import DurationDisplay from '../DurationDisplay.svelte';
8
- describe('DurationDisplay', () => {
9
- it('renders decimal hours to one decimal place', () => {
10
- render(DurationDisplay, { props: { hours: 2.5 } });
11
- expect(screen.getByText('2.5')).toBeInTheDocument();
12
- });
13
- it('renders an HH:MM value in hhmm format', () => {
14
- render(DurationDisplay, { props: { hours: 1.5, format: 'hhmm' } });
15
- expect(screen.getByText('1:30')).toBeInTheDocument();
16
- });
17
- // #2605: `showLabel` used to be inert. `decimal` rendered `h` whatever the
18
- // prop said, and `hhmm` rendered an empty `<span class="unit">` instead of a
19
- // label. These pin the unit to the prop, and pin the unchanged defaults so a
20
- // later change to them has to be deliberate.
21
- describe('showLabel', () => {
22
- it('renders the decimal unit by default', () => {
23
- const { container } = render(DurationDisplay, { props: { hours: 2.5 } });
24
- expect(container.querySelector('.unit')?.textContent).toBe('h');
25
- });
26
- it('renders the decimal unit when showLabel is set', () => {
27
- const { container } = render(DurationDisplay, {
28
- props: { hours: 2.5, showLabel: true },
29
- });
30
- expect(container.querySelector('.unit')?.textContent).toBe('h');
31
- });
32
- it('drops the decimal unit when showLabel is cleared', () => {
33
- const { container } = render(DurationDisplay, {
34
- props: { hours: 2.5, showLabel: false },
35
- });
36
- expect(container.querySelector('.unit')).toBeNull();
37
- expect(container.textContent?.trim()).toBe('2.5');
38
- });
39
- it.each([
40
- undefined,
41
- true,
42
- false,
43
- ])('renders no unit element in hhmm format with showLabel=%s', (showLabel) => {
44
- const { container } = render(DurationDisplay, {
45
- props: { hours: 1.5, format: 'hhmm', showLabel },
46
- });
47
- expect(container.querySelector('.unit')).toBeNull();
48
- expect(container.textContent?.trim()).toBe('1:30');
49
- });
50
- });
51
- it('is axe-clean', async () => {
52
- const { container } = render(DurationDisplay, {
53
- props: { hours: 8, showLabel: true },
54
- });
55
- await expectNoA11yViolations(container);
56
- });
57
- });
@@ -1,114 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * Component coverage for TimeEntryApprovalQueue via the shared S11 harness
4
- * (#1416): rendering, the approve/reject callbacks (reject collects a
5
- * reason first), and accessibility.
6
- */
7
- import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
8
- import { describe, expect, it, vi } from 'vitest';
9
- import TimeEntryApprovalQueue from '../TimeEntryApprovalQueue.svelte';
10
- function entryView(overrides = {}) {
11
- return {
12
- id: 'te-1',
13
- date: '2026-07-01',
14
- hours: 1.5,
15
- description: 'Debugged the login loop',
16
- status: 'submitted',
17
- amount: 18000, // $180.00 in cents (#2401)
18
- currency: 'USD',
19
- workerName: 'Ada',
20
- hourlyRate: 12000, // $120.00/h in cents
21
- source: 'timer',
22
- participantKind: 'human',
23
- ...overrides,
24
- };
25
- }
26
- describe('TimeEntryApprovalQueue', () => {
27
- it('renders date, hours, description, worker, status, and amount', async () => {
28
- const { container } = render(TimeEntryApprovalQueue, {
29
- props: { entries: [entryView()] },
30
- });
31
- expect(screen.getByText('2026-07-01')).toBeInTheDocument();
32
- expect(screen.getByText('1.5h')).toBeInTheDocument();
33
- expect(screen.getByText('Debugged the login loop')).toBeInTheDocument();
34
- expect(screen.getByText('Ada')).toBeInTheDocument();
35
- expect(screen.getByText('submitted')).toBeInTheDocument();
36
- expect(screen.getByText('180.00')).toBeInTheDocument();
37
- await expectNoA11yViolations(container);
38
- });
39
- it('omits the amount when no charge has derived yet', () => {
40
- render(TimeEntryApprovalQueue, {
41
- props: { entries: [entryView({ amount: undefined })] },
42
- });
43
- expect(screen.queryByText('180.00')).not.toBeInTheDocument();
44
- });
45
- it('scales by the currency exponent, not a hard-coded 100', () => {
46
- // ¥18000 is 18000 minor units — JPY has no minor unit — so dividing by 100
47
- // here would understate the charge a hundredfold (#2401).
48
- render(TimeEntryApprovalQueue, {
49
- props: { entries: [entryView({ currency: 'JPY' })] },
50
- });
51
- expect(screen.getByText('18000')).toBeInTheDocument();
52
- expect(screen.queryByText('180.00')).not.toBeInTheDocument();
53
- });
54
- it('invokes onapprove with the entry id', async () => {
55
- const onapprove = vi.fn();
56
- render(TimeEntryApprovalQueue, {
57
- props: { entries: [entryView()], onapprove },
58
- });
59
- await userEvent.click(screen.getByRole('button', {
60
- name: 'Approve time entry: Debugged the login loop',
61
- }));
62
- expect(onapprove).toHaveBeenCalledWith('te-1');
63
- });
64
- it('collects a reason before invoking onreject', async () => {
65
- const onreject = vi.fn();
66
- const { container } = render(TimeEntryApprovalQueue, {
67
- props: { entries: [entryView()], onreject },
68
- });
69
- await userEvent.click(screen.getByRole('button', {
70
- name: 'Reject time entry: Debugged the login loop',
71
- }));
72
- const confirmButton = screen.getByRole('button', {
73
- name: 'Confirm rejection of: Debugged the login loop',
74
- });
75
- expect(confirmButton).toBeDisabled();
76
- await userEvent.type(screen.getByRole('textbox', {
77
- name: 'Rejection reason for: Debugged the login loop',
78
- }), 'duplicate entry');
79
- await expectNoA11yViolations(container);
80
- await userEvent.click(confirmButton);
81
- expect(onreject).toHaveBeenCalledWith('te-1', 'duplicate entry');
82
- });
83
- it('cancelling a rejection restores the actions without firing onreject', async () => {
84
- const onreject = vi.fn();
85
- render(TimeEntryApprovalQueue, {
86
- props: { entries: [entryView()], onreject },
87
- });
88
- await userEvent.click(screen.getByRole('button', {
89
- name: 'Reject time entry: Debugged the login loop',
90
- }));
91
- await userEvent.click(screen.getByRole('button', {
92
- name: 'Cancel rejecting: Debugged the login loop',
93
- }));
94
- expect(onreject).not.toHaveBeenCalled();
95
- expect(screen.getByRole('button', {
96
- name: 'Approve time entry: Debugged the login loop',
97
- })).toBeInTheDocument();
98
- });
99
- it('shows no actions on rows that are not submitted', () => {
100
- render(TimeEntryApprovalQueue, {
101
- props: {
102
- entries: [entryView({ id: 'te-2', status: 'approved' })],
103
- onapprove: vi.fn(),
104
- },
105
- });
106
- expect(screen.queryByRole('button')).not.toBeInTheDocument();
107
- });
108
- it('shows the empty message when there are no entries', () => {
109
- render(TimeEntryApprovalQueue, {
110
- props: { entries: [], emptyMessage: 'Nothing awaiting review' },
111
- });
112
- expect(screen.getByText('Nothing awaiting review')).toBeInTheDocument();
113
- });
114
- });
@@ -1,45 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * Component coverage for TimeEntryCard via the shared S11 harness (#1416).
4
- */
5
- import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
6
- import { describe, expect, it, vi } from 'vitest';
7
- import TimeEntryCard from '../TimeEntryCard.svelte';
8
- const entry = {
9
- id: 'e1',
10
- date: '2026-06-01',
11
- description: 'Implement feature',
12
- status: 'draft',
13
- hours: 3,
14
- hourlyRate: 10000, // $100.00/h in cents (#2401)
15
- amount: 30000, // 3 h x $100.00
16
- workerName: 'Ada Lovelace',
17
- };
18
- describe('TimeEntryCard', () => {
19
- it('renders the entry description, status, and worker', () => {
20
- render(TimeEntryCard, { props: { entry } });
21
- expect(screen.getByText('Implement feature')).toBeInTheDocument();
22
- expect(screen.getByText('DRAFT')).toBeInTheDocument();
23
- expect(screen.getByText('Ada Lovelace')).toBeInTheDocument();
24
- });
25
- it('forwards checkbox selection through onselect', async () => {
26
- const onselect = vi.fn();
27
- render(TimeEntryCard, {
28
- props: { entry, selectable: true, onselect },
29
- });
30
- await userEvent.click(screen.getByLabelText('Select time entry for Implement feature'));
31
- expect(onselect).toHaveBeenCalledWith('e1', true);
32
- });
33
- it('invokes onclick when the card is activated', async () => {
34
- const onclick = vi.fn();
35
- render(TimeEntryCard, { props: { entry, onclick } });
36
- await userEvent.click(screen.getByText('Implement feature'));
37
- expect(onclick).toHaveBeenCalledTimes(1);
38
- });
39
- it('is axe-clean', async () => {
40
- const { container } = render(TimeEntryCard, {
41
- props: { entry, selectable: true, onselect: vi.fn() },
42
- });
43
- await expectNoA11yViolations(container);
44
- });
45
- });
@@ -1,50 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * Component coverage for TimeEntryList via the shared S11 harness (#1416).
4
- */
5
- import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
6
- import { describe, expect, it, vi } from 'vitest';
7
- import TimeEntryList from '../TimeEntryList.svelte';
8
- const entries = [
9
- {
10
- id: 'e1',
11
- date: '2026-06-01',
12
- description: 'First task',
13
- status: 'draft',
14
- hours: 2,
15
- },
16
- {
17
- id: 'e2',
18
- date: '2026-06-02',
19
- description: 'Second task',
20
- status: 'draft',
21
- hours: 4,
22
- },
23
- ];
24
- describe('TimeEntryList', () => {
25
- it('renders a card per entry', () => {
26
- render(TimeEntryList, { props: { entries } });
27
- expect(screen.getByText('First task')).toBeInTheDocument();
28
- expect(screen.getByText('Second task')).toBeInTheDocument();
29
- });
30
- it('shows the empty message when there are no entries', () => {
31
- render(TimeEntryList, {
32
- props: { entries: [], emptyMessage: 'No time entries' },
33
- });
34
- expect(screen.getByText('No time entries')).toBeInTheDocument();
35
- });
36
- it('selects all entries via the header checkbox', async () => {
37
- const onselectionchange = vi.fn();
38
- render(TimeEntryList, {
39
- props: { entries, selectable: true, onselectionchange },
40
- });
41
- await userEvent.click(screen.getByLabelText('Select all time entries'));
42
- expect(onselectionchange).toHaveBeenCalledWith(['e1', 'e2']);
43
- });
44
- it('is axe-clean', async () => {
45
- const { container } = render(TimeEntryList, {
46
- props: { entries, selectable: true, onselectionchange: vi.fn() },
47
- });
48
- await expectNoA11yViolations(container);
49
- });
50
- });
@@ -1,39 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * Component coverage for TimeSummary via the shared S11 harness (#1416).
4
- */
5
- import { expectNoA11yViolations, render, screen, } from '@happyvertical/smrt-vitest/svelte';
6
- import { describe, expect, it } from 'vitest';
7
- import TimeSummary from '../TimeSummary.svelte';
8
- describe('TimeSummary', () => {
9
- it('renders the total hours and value cards', () => {
10
- render(TimeSummary, {
11
- props: { totalHours: 12, totalAmount: 600, entryCount: 3 },
12
- });
13
- expect(screen.getByText('Total Hours')).toBeInTheDocument();
14
- expect(screen.getByText('Total Value')).toBeInTheDocument();
15
- expect(screen.getByText('3 entries')).toBeInTheDocument();
16
- });
17
- it('shows pending and approved cards when enabled and non-zero', () => {
18
- render(TimeSummary, {
19
- props: {
20
- totalHours: 12,
21
- totalAmount: 600,
22
- pendingHours: 4,
23
- pendingAmount: 200,
24
- approvedHours: 8,
25
- approvedAmount: 400,
26
- showPending: true,
27
- showApproved: true,
28
- },
29
- });
30
- expect(screen.getByText('Pending Approval')).toBeInTheDocument();
31
- expect(screen.getByText('Approved')).toBeInTheDocument();
32
- });
33
- it('is axe-clean', async () => {
34
- const { container } = render(TimeSummary, {
35
- props: { totalHours: 12, totalAmount: 600 },
36
- });
37
- await expectNoA11yViolations(container);
38
- });
39
- });