@happyvertical/smrt-projects 0.52.0 → 0.53.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/AGENTS.md +20 -9
- package/README.md +13 -1
- package/SERVICE_TIME_MIGRATION.md +45 -3
- package/dist/index.js +123 -586
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +3 -1484
- package/dist/models/service-evidence.d.ts +13 -92
- package/dist/models/service-evidence.d.ts.map +1 -1
- package/dist/playground.js +2 -2
- package/dist/playground.js.map +1 -1
- package/dist/services/service-evidence-service.d.ts +6 -55
- package/dist/services/service-evidence-service.d.ts.map +1 -1
- package/dist/services/subscription-commercial-resolver.d.ts +1 -1
- package/dist/services/subscription-commercial-resolver.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +10 -1068
- package/dist/svelte/components/ApprovalActions.svelte +1 -1
- package/dist/svelte/components/ApprovalActions.svelte.d.ts +1 -1
- package/dist/svelte/components/ApprovalActions.svelte.d.ts.map +1 -1
- package/dist/svelte/components/__tests__/TimesheetsReexports.test.js +18 -0
- package/dist/svelte/i18n.d.ts +0 -9
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +0 -9
- package/dist/svelte/index.d.ts +2 -8
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +3 -4
- package/dist/svelte/playground.d.ts +7 -2
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +7 -2
- package/dist/svelte/utils.d.ts +4 -48
- package/dist/svelte/utils.d.ts.map +1 -1
- package/dist/svelte/utils.js +4 -56
- package/dist/svelte/utils.test.js +1 -1
- package/dist/types.d.ts +1 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -18
- package/dist/svelte/components/DurationDisplay.svelte +0 -81
- package/dist/svelte/components/DurationDisplay.svelte.d.ts +0 -20
- package/dist/svelte/components/DurationDisplay.svelte.d.ts.map +0 -1
- package/dist/svelte/components/TimeEntryCard.svelte +0 -304
- package/dist/svelte/components/TimeEntryCard.svelte.d.ts +0 -25
- package/dist/svelte/components/TimeEntryCard.svelte.d.ts.map +0 -1
- package/dist/svelte/components/TimeEntryList.svelte +0 -360
- package/dist/svelte/components/TimeEntryList.svelte.d.ts +0 -24
- package/dist/svelte/components/TimeEntryList.svelte.d.ts.map +0 -1
- package/dist/svelte/components/TimeSummary.svelte +0 -176
- package/dist/svelte/components/TimeSummary.svelte.d.ts +0 -30
- package/dist/svelte/components/TimeSummary.svelte.d.ts.map +0 -1
- package/dist/svelte/components/__tests__/DurationDisplay.test.js +0 -57
- package/dist/svelte/components/__tests__/TimeEntryCard.test.js +0 -45
- package/dist/svelte/components/__tests__/TimeEntryList.test.js +0 -50
- package/dist/svelte/components/__tests__/TimeSummary.test.js +0 -39
- package/dist/svelte/components/utils.d.ts +0 -51
- package/dist/svelte/components/utils.d.ts.map +0 -1
- package/dist/svelte/components/utils.js +0 -58
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TimeEntryList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/TimeEntryList.svelte.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,QAAQ,EAKb,KAAK,SAAS,EACf,MAAM,YAAY,CAAC;AAGpB,wCAAwC;AACxC,MAAM,WAAW,KAAK;IACpB,2CAA2C;IAC3C,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,0CAA0C;IAC1C,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC5C,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,iEAAiE;IACjE,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC;CAC3C;AA2ID,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* TimeSummary - Summary statistics for time entries
|
|
4
|
-
* Shows total hours, amounts, and pending items
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
8
|
-
import { M } from '../i18n.js';
|
|
9
|
-
import { type Currency, formatCurrency, formatHours } from './utils.js';
|
|
10
|
-
|
|
11
|
-
/** Props for TimeSummary component */
|
|
12
|
-
export interface Props {
|
|
13
|
-
/** Total hours across all time entries. */
|
|
14
|
-
totalHours: number;
|
|
15
|
-
/** Total charged or invoiced amount. */
|
|
16
|
-
totalAmount: number;
|
|
17
|
-
/** Hours awaiting approval. */
|
|
18
|
-
pendingHours?: number;
|
|
19
|
-
/** Amount awaiting approval. */
|
|
20
|
-
pendingAmount?: number;
|
|
21
|
-
/** Hours that have been approved. */
|
|
22
|
-
approvedHours?: number;
|
|
23
|
-
/** Amount that has been approved. */
|
|
24
|
-
approvedAmount?: number;
|
|
25
|
-
/** Number of time entries included in the summary. */
|
|
26
|
-
entryCount?: number;
|
|
27
|
-
/** Currency code for formatting amounts. */
|
|
28
|
-
currency?: Currency;
|
|
29
|
-
/** Shows the pending hours and amount card when there are pending items. */
|
|
30
|
-
showPending?: boolean;
|
|
31
|
-
/** Shows the approved hours and amount card when there are approved items. */
|
|
32
|
-
showApproved?: boolean;
|
|
33
|
-
/** Arranges summary cards in a grid or flexbox layout. */
|
|
34
|
-
layout?: 'horizontal' | 'grid';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let {
|
|
38
|
-
totalHours,
|
|
39
|
-
totalAmount,
|
|
40
|
-
pendingHours = 0,
|
|
41
|
-
pendingAmount = 0,
|
|
42
|
-
approvedHours = 0,
|
|
43
|
-
approvedAmount = 0,
|
|
44
|
-
entryCount,
|
|
45
|
-
currency = 'CAD',
|
|
46
|
-
showPending = true,
|
|
47
|
-
showApproved = false,
|
|
48
|
-
layout = 'grid',
|
|
49
|
-
}: Props = $props();
|
|
50
|
-
|
|
51
|
-
const { t } = useI18n();
|
|
52
|
-
</script>
|
|
53
|
-
|
|
54
|
-
<div class="time-summary" class:horizontal={layout === 'horizontal'}>
|
|
55
|
-
<div class="summary-card">
|
|
56
|
-
<span class="label">{t(M['projects.time_summary.total_hours'])}</span>
|
|
57
|
-
<span class="value">{formatHours(totalHours)}</span>
|
|
58
|
-
{#if entryCount !== undefined}
|
|
59
|
-
<span class="count">{entryCount} {entryCount === 1 ? 'entry' : 'entries'}</span>
|
|
60
|
-
{/if}
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<div class="summary-card">
|
|
64
|
-
<span class="label">{t(M['projects.time_summary.total_value'])}</span>
|
|
65
|
-
<span class="value">{formatCurrency(totalAmount, currency)}</span>
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
{#if showPending && (pendingHours > 0 || pendingAmount > 0)}
|
|
69
|
-
<div class="summary-card highlight">
|
|
70
|
-
<span class="label">{t(M['projects.time_summary.pending_approval'])}</span>
|
|
71
|
-
<span class="value">{formatHours(pendingHours)}</span>
|
|
72
|
-
<span class="sub-value">{formatCurrency(pendingAmount, currency)}</span>
|
|
73
|
-
</div>
|
|
74
|
-
{/if}
|
|
75
|
-
|
|
76
|
-
{#if showApproved && (approvedHours > 0 || approvedAmount > 0)}
|
|
77
|
-
<div class="summary-card success">
|
|
78
|
-
<span class="label">Approved</span>
|
|
79
|
-
<span class="value">{formatHours(approvedHours)}</span>
|
|
80
|
-
<span class="sub-value">{formatCurrency(approvedAmount, currency)}</span>
|
|
81
|
-
</div>
|
|
82
|
-
{/if}
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
<style>
|
|
86
|
-
.time-summary {
|
|
87
|
-
display: grid;
|
|
88
|
-
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
89
|
-
gap: 1rem;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.time-summary.horizontal {
|
|
93
|
-
display: flex;
|
|
94
|
-
flex-wrap: wrap;
|
|
95
|
-
gap: 1rem;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.time-summary.horizontal .summary-card {
|
|
99
|
-
flex: 1 1 140px;
|
|
100
|
-
min-width: 140px;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.summary-card {
|
|
104
|
-
background: var(--smrt-color-surface);
|
|
105
|
-
border: 1px solid var(--smrt-color-outline-variant);
|
|
106
|
-
border-radius: var(--smrt-radius-medium, 12px);
|
|
107
|
-
padding: 1rem;
|
|
108
|
-
display: flex;
|
|
109
|
-
flex-direction: column;
|
|
110
|
-
gap: 0.25rem;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.summary-card.highlight {
|
|
114
|
-
background: var(--smrt-color-tertiary-container);
|
|
115
|
-
border-color: var(--smrt-color-tertiary);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.summary-card.success {
|
|
119
|
-
background: var(--smrt-color-primary-container);
|
|
120
|
-
border-color: var(--smrt-color-primary);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.label {
|
|
124
|
-
font-size: var(--smrt-typography-label-small-size, 0.75rem);
|
|
125
|
-
font-weight: var(--smrt-typography-label-small-weight, 500);
|
|
126
|
-
color: var(--smrt-color-on-surface-variant);
|
|
127
|
-
text-transform: uppercase;
|
|
128
|
-
letter-spacing: var(--smrt-typography-label-small-tracking, 0.5px);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.value {
|
|
132
|
-
font-size: var(--smrt-typography-headline-small-size, 1.5rem);
|
|
133
|
-
font-weight: var(--smrt-typography-headline-small-weight, 400);
|
|
134
|
-
color: var(--smrt-color-on-surface);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.sub-value {
|
|
138
|
-
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
139
|
-
color: var(--smrt-color-on-surface-variant);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.count {
|
|
143
|
-
font-size: var(--smrt-typography-body-small-size, 0.75rem);
|
|
144
|
-
color: var(--smrt-color-on-surface-variant);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.highlight .label {
|
|
148
|
-
color: var(--smrt-color-on-tertiary-container);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.highlight .value {
|
|
152
|
-
color: var(--smrt-color-tertiary);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.highlight .sub-value {
|
|
156
|
-
color: var(--smrt-color-on-tertiary-container);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.success .label {
|
|
160
|
-
color: var(--smrt-color-on-primary-container);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
.success .value {
|
|
164
|
-
color: var(--smrt-color-primary);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.success .sub-value {
|
|
168
|
-
color: var(--smrt-color-on-primary-container);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
@media (max-width: 480px) {
|
|
172
|
-
.time-summary {
|
|
173
|
-
grid-template-columns: 1fr 1fr;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
</style>
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { type Currency } from './utils.js';
|
|
2
|
-
/** Props for TimeSummary component */
|
|
3
|
-
export interface Props {
|
|
4
|
-
/** Total hours across all time entries. */
|
|
5
|
-
totalHours: number;
|
|
6
|
-
/** Total charged or invoiced amount. */
|
|
7
|
-
totalAmount: number;
|
|
8
|
-
/** Hours awaiting approval. */
|
|
9
|
-
pendingHours?: number;
|
|
10
|
-
/** Amount awaiting approval. */
|
|
11
|
-
pendingAmount?: number;
|
|
12
|
-
/** Hours that have been approved. */
|
|
13
|
-
approvedHours?: number;
|
|
14
|
-
/** Amount that has been approved. */
|
|
15
|
-
approvedAmount?: number;
|
|
16
|
-
/** Number of time entries included in the summary. */
|
|
17
|
-
entryCount?: number;
|
|
18
|
-
/** Currency code for formatting amounts. */
|
|
19
|
-
currency?: Currency;
|
|
20
|
-
/** Shows the pending hours and amount card when there are pending items. */
|
|
21
|
-
showPending?: boolean;
|
|
22
|
-
/** Shows the approved hours and amount card when there are approved items. */
|
|
23
|
-
showApproved?: boolean;
|
|
24
|
-
/** Arranges summary cards in a grid or flexbox layout. */
|
|
25
|
-
layout?: 'horizontal' | 'grid';
|
|
26
|
-
}
|
|
27
|
-
declare const TimeSummary: import("svelte").Component<Props, {}, "">;
|
|
28
|
-
type TimeSummary = ReturnType<typeof TimeSummary>;
|
|
29
|
-
export default TimeSummary;
|
|
30
|
-
//# sourceMappingURL=TimeSummary.svelte.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TimeSummary.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/TimeSummary.svelte.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,QAAQ,EAA+B,MAAM,YAAY,CAAC;AAGxE,sCAAsC;AACtC,MAAM,WAAW,KAAK;IACpB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;CAChC;AA6DD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -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,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
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for time tracking components
|
|
3
|
-
*/
|
|
4
|
-
export type Currency = 'CAD' | 'USD';
|
|
5
|
-
export type TimeEntryStatus = 'draft' | 'submitted' | 'approved' | 'rejected';
|
|
6
|
-
/** Alias for TimeEntryStatus for approval workflows */
|
|
7
|
-
export type ApprovalStatus = TimeEntryStatus;
|
|
8
|
-
/**
|
|
9
|
-
* Time entry data structure
|
|
10
|
-
*/
|
|
11
|
-
export interface TimeEntry {
|
|
12
|
-
id: string;
|
|
13
|
-
date: Date | string;
|
|
14
|
-
hours: number;
|
|
15
|
-
description: string;
|
|
16
|
-
status: TimeEntryStatus;
|
|
17
|
-
/** Integer minor units of the display currency — $19.99 is 1999 (#2401). */
|
|
18
|
-
amount?: number;
|
|
19
|
-
workerName?: string;
|
|
20
|
-
mileage?: number;
|
|
21
|
-
/** Minor units per hour, so `hours * hourlyRate` is minor units (#2401). */
|
|
22
|
-
hourlyRate?: number;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Status color mapping using M3 design tokens
|
|
26
|
-
*/
|
|
27
|
-
export declare const statusColors: Record<TimeEntryStatus, string>;
|
|
28
|
-
/**
|
|
29
|
-
* Format a date for display (e.g., "Jan 5")
|
|
30
|
-
*/
|
|
31
|
-
export declare function formatDate(date: Date | string): string;
|
|
32
|
-
/**
|
|
33
|
-
* Format a minor-units amount for display.
|
|
34
|
-
*
|
|
35
|
-
* Money in this package is integer minor units (#2401) — `$19.99` is `1999` —
|
|
36
|
-
* and `Intl.NumberFormat` expects major units, so the scale is undone here.
|
|
37
|
-
* Without it a $19.99 charge renders as $1,999.00.
|
|
38
|
-
*
|
|
39
|
-
* The currency's own minor-unit exponent is used rather than a hard-coded 100,
|
|
40
|
-
* so zero-decimal currencies (JPY, KRW) are not divided by anything.
|
|
41
|
-
*/
|
|
42
|
-
export declare function formatCurrency(amount: number, currency?: Currency): string;
|
|
43
|
-
/**
|
|
44
|
-
* Format hours for display (e.g., "8.5h")
|
|
45
|
-
*/
|
|
46
|
-
export declare function formatHours(hours: number): string;
|
|
47
|
-
/**
|
|
48
|
-
* Format hours in HH:MM format (e.g., "8:30")
|
|
49
|
-
*/
|
|
50
|
-
export declare function formatHoursHHMM(hours: number): string;
|
|
51
|
-
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,KAAK,CAAC;AAErC,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAAC;AAE9E,uDAAuD;AACvD,MAAM,MAAM,cAAc,GAAG,eAAe,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,eAAe,CAAC;IACxB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAKxD,CAAC;AAEF;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAGtD;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,GAAE,QAAgB,GACzB,MAAM,CAcR;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIrD"}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for time tracking components
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Status color mapping using M3 design tokens
|
|
6
|
-
*/
|
|
7
|
-
export const statusColors = {
|
|
8
|
-
draft: 'var(--md-sys-color-outline)',
|
|
9
|
-
submitted: 'var(--md-sys-color-tertiary)',
|
|
10
|
-
approved: 'var(--md-sys-color-primary)',
|
|
11
|
-
rejected: 'var(--md-sys-color-error)',
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* Format a date for display (e.g., "Jan 5")
|
|
15
|
-
*/
|
|
16
|
-
export function formatDate(date) {
|
|
17
|
-
const d = typeof date === 'string' ? new Date(date) : date;
|
|
18
|
-
return d.toLocaleDateString('en-CA', { month: 'short', day: 'numeric' });
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Format a minor-units amount for display.
|
|
22
|
-
*
|
|
23
|
-
* Money in this package is integer minor units (#2401) — `$19.99` is `1999` —
|
|
24
|
-
* and `Intl.NumberFormat` expects major units, so the scale is undone here.
|
|
25
|
-
* Without it a $19.99 charge renders as $1,999.00.
|
|
26
|
-
*
|
|
27
|
-
* The currency's own minor-unit exponent is used rather than a hard-coded 100,
|
|
28
|
-
* so zero-decimal currencies (JPY, KRW) are not divided by anything.
|
|
29
|
-
*/
|
|
30
|
-
export function formatCurrency(amount, currency = 'CAD') {
|
|
31
|
-
// No fraction-digit override: the currency's own exponent is both the scale
|
|
32
|
-
// to undo and the precision to print. Forcing `minimumFractionDigits: 2`
|
|
33
|
-
// (as this did before minor units) would pin `maximumFractionDigits` to 2 as
|
|
34
|
-
// well, so a zero-decimal currency like JPY would report a scale of 2 — and
|
|
35
|
-
// then render `¥1,999.00` for a currency that has no fractional part at all.
|
|
36
|
-
// For CAD/USD the resolved default is already 2, so their output is
|
|
37
|
-
// unchanged.
|
|
38
|
-
const format = new Intl.NumberFormat('en-CA', {
|
|
39
|
-
style: 'currency',
|
|
40
|
-
currency,
|
|
41
|
-
});
|
|
42
|
-
const digits = format.resolvedOptions().maximumFractionDigits ?? 2;
|
|
43
|
-
return format.format(amount / 10 ** digits);
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Format hours for display (e.g., "8.5h")
|
|
47
|
-
*/
|
|
48
|
-
export function formatHours(hours) {
|
|
49
|
-
return `${hours.toFixed(1)}h`;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Format hours in HH:MM format (e.g., "8:30")
|
|
53
|
-
*/
|
|
54
|
-
export function formatHoursHHMM(hours) {
|
|
55
|
-
const h = Math.floor(hours);
|
|
56
|
-
const m = Math.round((hours - h) * 60);
|
|
57
|
-
return `${h}:${m.toString().padStart(2, '0')}`;
|
|
58
|
-
}
|