@happyvertical/smrt-template-sveltekit 0.56.0 → 0.56.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.
- package/mcp-apps-template/package.dependencies.json +2 -2
- package/package.json +20 -20
- package/template/package.json +12 -12
- package/template/src/lib/components/MaterializedReportDrilldown.svelte +135 -0
- package/template/src/lib/materialized-report-drilldown.ts +55 -0
- package/template/src/lib/server/materialized-reports.ts +12 -0
- package/template/src/routes/reports/materialized/[id]/+page.server.ts +46 -0
- package/template/src/routes/reports/materialized/[id]/+page.svelte +97 -0
- package/template/src/routes/reports/materialized/[id]/drilldown/+server.ts +24 -0
- package/template.config.js +12 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-template-sveltekit",
|
|
3
|
-
"version": "0.56.
|
|
3
|
+
"version": "0.56.2",
|
|
4
4
|
"description": "Minimal SvelteKit project template with s-m-r-t framework integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -32,26 +32,26 @@
|
|
|
32
32
|
"directory": "packages/template-sveltekit"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@happyvertical/smrt-core": "0.56.
|
|
35
|
+
"@happyvertical/smrt-core": "0.56.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@happyvertical/smrt-app-cli": "0.56.
|
|
39
|
-
"@happyvertical/smrt-app-mcp": "0.56.
|
|
40
|
-
"@happyvertical/smrt-app-runtime": "0.56.
|
|
41
|
-
"@happyvertical/smrt-assets": "0.56.
|
|
42
|
-
"@happyvertical/smrt-chat": "0.56.
|
|
43
|
-
"@happyvertical/smrt-cli": "0.56.
|
|
44
|
-
"@happyvertical/smrt-config": "0.56.
|
|
45
|
-
"@happyvertical/smrt-jobs": "0.56.
|
|
46
|
-
"@happyvertical/smrt-mcp-openai": "0.56.
|
|
47
|
-
"@happyvertical/smrt-playbooks": "0.56.
|
|
48
|
-
"@happyvertical/smrt-profiles": "0.56.
|
|
49
|
-
"@happyvertical/smrt-scanner": "0.56.
|
|
50
|
-
"@happyvertical/smrt-svelte": "0.56.
|
|
51
|
-
"@happyvertical/smrt-tenancy": "0.56.
|
|
52
|
-
"@happyvertical/smrt-ui": "0.56.
|
|
53
|
-
"@happyvertical/smrt-users": "0.56.
|
|
54
|
-
"@happyvertical/smrt-web": "0.56.
|
|
38
|
+
"@happyvertical/smrt-app-cli": "0.56.2",
|
|
39
|
+
"@happyvertical/smrt-app-mcp": "0.56.2",
|
|
40
|
+
"@happyvertical/smrt-app-runtime": "0.56.2",
|
|
41
|
+
"@happyvertical/smrt-assets": "0.56.2",
|
|
42
|
+
"@happyvertical/smrt-chat": "0.56.2",
|
|
43
|
+
"@happyvertical/smrt-cli": "0.56.2",
|
|
44
|
+
"@happyvertical/smrt-config": "0.56.2",
|
|
45
|
+
"@happyvertical/smrt-jobs": "0.56.2",
|
|
46
|
+
"@happyvertical/smrt-mcp-openai": "0.56.2",
|
|
47
|
+
"@happyvertical/smrt-playbooks": "0.56.2",
|
|
48
|
+
"@happyvertical/smrt-profiles": "0.56.2",
|
|
49
|
+
"@happyvertical/smrt-scanner": "0.56.2",
|
|
50
|
+
"@happyvertical/smrt-svelte": "0.56.2",
|
|
51
|
+
"@happyvertical/smrt-tenancy": "0.56.2",
|
|
52
|
+
"@happyvertical/smrt-ui": "0.56.2",
|
|
53
|
+
"@happyvertical/smrt-users": "0.56.2",
|
|
54
|
+
"@happyvertical/smrt-web": "0.56.2",
|
|
55
55
|
"@happyvertical/sql": "^0.89.12",
|
|
56
56
|
"@modelcontextprotocol/client": "2.0.0",
|
|
57
57
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"typescript": "6.0.3",
|
|
66
66
|
"vite": "8.1.4",
|
|
67
67
|
"vitest": "4.1.10",
|
|
68
|
-
"@happyvertical/smrt-reports": "0.56.
|
|
68
|
+
"@happyvertical/smrt-reports": "0.56.2"
|
|
69
69
|
},
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"registry": "https://registry.npmjs.org",
|
package/template/package.json
CHANGED
|
@@ -39,21 +39,21 @@
|
|
|
39
39
|
"svelte-check": "^4.7.6",
|
|
40
40
|
"typescript": "6.0.3",
|
|
41
41
|
"vite": "8.3.0",
|
|
42
|
-
"@happyvertical/smrt-cli": "^0.56.
|
|
42
|
+
"@happyvertical/smrt-cli": "^0.56.2"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@happyvertical/smrt-app-runtime": "^0.56.
|
|
46
|
-
"@happyvertical/smrt-config": "^0.56.
|
|
47
|
-
"@happyvertical/smrt-core": "^0.56.
|
|
48
|
-
"@happyvertical/smrt-jobs": "^0.56.
|
|
49
|
-
"@happyvertical/smrt-profiles": "^0.56.
|
|
50
|
-
"@happyvertical/smrt-svelte": "^0.56.
|
|
51
|
-
"@happyvertical/smrt-tenancy": "^0.56.
|
|
52
|
-
"@happyvertical/smrt-ui": "^0.56.
|
|
53
|
-
"@happyvertical/smrt-users": "^0.56.
|
|
54
|
-
"@happyvertical/smrt-web": "^0.56.
|
|
45
|
+
"@happyvertical/smrt-app-runtime": "^0.56.2",
|
|
46
|
+
"@happyvertical/smrt-config": "^0.56.2",
|
|
47
|
+
"@happyvertical/smrt-core": "^0.56.2",
|
|
48
|
+
"@happyvertical/smrt-jobs": "^0.56.2",
|
|
49
|
+
"@happyvertical/smrt-profiles": "^0.56.2",
|
|
50
|
+
"@happyvertical/smrt-svelte": "^0.56.2",
|
|
51
|
+
"@happyvertical/smrt-tenancy": "^0.56.2",
|
|
52
|
+
"@happyvertical/smrt-ui": "^0.56.2",
|
|
53
|
+
"@happyvertical/smrt-users": "^0.56.2",
|
|
54
|
+
"@happyvertical/smrt-web": "^0.56.2",
|
|
55
55
|
"@happyvertical/sql": "^0.89.12",
|
|
56
56
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
57
|
-
"@happyvertical/smrt-reports": "^0.56.
|
|
57
|
+
"@happyvertical/smrt-reports": "^0.56.2"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { MaterializedReportDrilldownPresentation } from '@happyvertical/smrt-reports';
|
|
3
|
+
import { formatWidgetValue } from '@happyvertical/smrt-ui/charts/format';
|
|
4
|
+
import { DataTable, type DataTableColumn } from '@happyvertical/smrt-ui/data';
|
|
5
|
+
import { Alert } from '@happyvertical/smrt-ui/feedback';
|
|
6
|
+
import { Badge, Card } from '@happyvertical/smrt-ui/ui';
|
|
7
|
+
import { parseMaterializedReportDrilldown } from '$lib/materialized-report-drilldown';
|
|
8
|
+
|
|
9
|
+
interface ContributorRow {
|
|
10
|
+
id: string;
|
|
11
|
+
source: string;
|
|
12
|
+
identity: string;
|
|
13
|
+
date: string;
|
|
14
|
+
party: string;
|
|
15
|
+
value: string;
|
|
16
|
+
taxable: string;
|
|
17
|
+
tax: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
details: unknown;
|
|
22
|
+
locale?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let { details, locale = 'en' }: Props = $props();
|
|
26
|
+
let presentation = $derived(parseMaterializedReportDrilldown(details));
|
|
27
|
+
|
|
28
|
+
const columns: DataTableColumn<ContributorRow>[] = [
|
|
29
|
+
{ id: 'source', label: 'Source', accessor: 'source', responsive: { keepVisible: true } },
|
|
30
|
+
{ id: 'identity', label: 'Document or source', accessor: 'identity', responsive: { keepVisible: true } },
|
|
31
|
+
{ id: 'date', label: 'Date', accessor: 'date' },
|
|
32
|
+
{ id: 'party', label: 'Party', accessor: 'party' },
|
|
33
|
+
{ id: 'value', label: 'Selected measure', accessor: 'value', align: 'right' },
|
|
34
|
+
{ id: 'taxable', label: 'Taxable', accessor: 'taxable', align: 'right' },
|
|
35
|
+
{ id: 'tax', label: 'Tax', accessor: 'tax', align: 'right' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function currency(value: number | null | undefined, code: string): string {
|
|
39
|
+
return value === null || value === undefined
|
|
40
|
+
? '–'
|
|
41
|
+
: formatWidgetValue(value, { format: 'money', currency: code, locale });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function calendarDate(value: string | null | undefined): string {
|
|
45
|
+
return value?.slice(0, 10) || '–';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rows(value: MaterializedReportDrilldownPresentation): ContributorRow[] {
|
|
49
|
+
return value.contributors.map((contributor) => ({
|
|
50
|
+
id: contributor.id,
|
|
51
|
+
source: contributor.source,
|
|
52
|
+
identity: contributor.identity,
|
|
53
|
+
date: calendarDate(contributor.date),
|
|
54
|
+
party: contributor.party || '–',
|
|
55
|
+
value: currency(contributor.values.valueMinor, value.currency),
|
|
56
|
+
taxable: currency(contributor.values.taxableMinor, value.currency),
|
|
57
|
+
tax: currency(contributor.values.taxMinor, value.currency),
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<section class="drilldown" aria-label="Report drilldown" data-smrt-component="MaterializedReportDrilldown">
|
|
63
|
+
{#if presentation}
|
|
64
|
+
<Card>
|
|
65
|
+
<div class="heading">
|
|
66
|
+
<div>
|
|
67
|
+
<h2>Report drilldown</h2>
|
|
68
|
+
<p>Selected measure: <strong>{presentation.selectedMeasure.label}</strong></p>
|
|
69
|
+
</div>
|
|
70
|
+
<Badge variant={presentation.complete ? 'success' : 'warning'}>
|
|
71
|
+
{presentation.complete ? 'Complete' : 'Needs attention'}
|
|
72
|
+
</Badge>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{#each presentation.warnings as warning (warning.code)}
|
|
76
|
+
<Alert variant={warning.kind === 'drift' ? 'warning' : 'info'} title={warning.kind === 'drift' ? 'Source data changed' : 'Detail is incomplete'}>
|
|
77
|
+
{warning.message}
|
|
78
|
+
</Alert>
|
|
79
|
+
{/each}
|
|
80
|
+
|
|
81
|
+
{#if !presentation.complete && presentation.warnings.length === 0}
|
|
82
|
+
<Alert variant="warning" title="Detail is incomplete">
|
|
83
|
+
This report cannot establish the selected amount exactly.
|
|
84
|
+
</Alert>
|
|
85
|
+
{/if}
|
|
86
|
+
|
|
87
|
+
<DataTable
|
|
88
|
+
data={rows(presentation)}
|
|
89
|
+
{columns}
|
|
90
|
+
rowKey="id"
|
|
91
|
+
caption="Report contributors"
|
|
92
|
+
responsiveMode="hide-columns"
|
|
93
|
+
/>
|
|
94
|
+
</Card>
|
|
95
|
+
{:else}
|
|
96
|
+
<Alert variant="error" title="Report drilldown unavailable">
|
|
97
|
+
This report returned a detail that this application cannot display safely.
|
|
98
|
+
</Alert>
|
|
99
|
+
{/if}
|
|
100
|
+
</section>
|
|
101
|
+
|
|
102
|
+
<style>
|
|
103
|
+
@layer smrt.theme {
|
|
104
|
+
.drilldown {
|
|
105
|
+
width: min(100%, 72rem);
|
|
106
|
+
margin-inline: auto;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.heading {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: start;
|
|
112
|
+
justify-content: space-between;
|
|
113
|
+
gap: var(--smrt-spacing-4);
|
|
114
|
+
margin-bottom: var(--smrt-spacing-5);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
h2,
|
|
118
|
+
p {
|
|
119
|
+
margin: 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
h2 {
|
|
123
|
+
font: var(--smrt-typography-title-medium-font);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
p {
|
|
127
|
+
margin-top: var(--smrt-spacing-1);
|
|
128
|
+
color: var(--smrt-color-on-surface-variant);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
:global(.smrt-alert__root) {
|
|
132
|
+
margin-block: var(--smrt-spacing-3);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { MaterializedReportDrilldownPresentation } from '@happyvertical/smrt-reports';
|
|
2
|
+
|
|
3
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
4
|
+
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
|
|
6
|
+
const nonemptyText = (value: unknown): value is string =>
|
|
7
|
+
typeof value === 'string' && value.trim().length > 0;
|
|
8
|
+
|
|
9
|
+
const optionalText = (value: unknown): value is string | null | undefined =>
|
|
10
|
+
value === undefined || value === null || typeof value === 'string';
|
|
11
|
+
|
|
12
|
+
const minor = (value: unknown): value is number | null | undefined =>
|
|
13
|
+
value === undefined || value === null || Number.isSafeInteger(value);
|
|
14
|
+
|
|
15
|
+
/** Validate the versioned host payload before it reaches the generic UI. */
|
|
16
|
+
export function parseMaterializedReportDrilldown(
|
|
17
|
+
value: unknown,
|
|
18
|
+
): MaterializedReportDrilldownPresentation | undefined {
|
|
19
|
+
if (!isRecord(value) || value.version !== 1 || !isRecord(value.selectedMeasure))
|
|
20
|
+
return undefined;
|
|
21
|
+
if (
|
|
22
|
+
!nonemptyText(value.selectedMeasure.id) ||
|
|
23
|
+
!nonemptyText(value.selectedMeasure.label) ||
|
|
24
|
+
!nonemptyText(value.currency) ||
|
|
25
|
+
!Array.isArray(value.contributors) ||
|
|
26
|
+
!Array.isArray(value.warnings) ||
|
|
27
|
+
typeof value.complete !== 'boolean'
|
|
28
|
+
)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (!/^[A-Za-z]{3}$/.test(value.currency)) return undefined;
|
|
31
|
+
for (const contributor of value.contributors) {
|
|
32
|
+
if (!isRecord(contributor) || !isRecord(contributor.values)) return undefined;
|
|
33
|
+
if (
|
|
34
|
+
!nonemptyText(contributor.id) ||
|
|
35
|
+
!nonemptyText(contributor.source) ||
|
|
36
|
+
!nonemptyText(contributor.identity) ||
|
|
37
|
+
!optionalText(contributor.date) ||
|
|
38
|
+
!optionalText(contributor.party) ||
|
|
39
|
+
!minor(contributor.values.valueMinor) ||
|
|
40
|
+
!minor(contributor.values.taxableMinor) ||
|
|
41
|
+
!minor(contributor.values.taxMinor)
|
|
42
|
+
)
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
for (const warning of value.warnings) {
|
|
46
|
+
if (
|
|
47
|
+
!isRecord(warning) ||
|
|
48
|
+
!nonemptyText(warning.code) ||
|
|
49
|
+
!nonemptyText(warning.message) ||
|
|
50
|
+
(warning.kind !== 'incomplete' && warning.kind !== 'drift')
|
|
51
|
+
)
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return value as unknown as MaterializedReportDrilldownPresentation;
|
|
55
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MaterializedReportHost } from '@happyvertical/smrt-reports';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Server-only materialized-report registrations for this application.
|
|
5
|
+
*
|
|
6
|
+
* Add fixed, application-owned hosts here. A commerce app uses
|
|
7
|
+
* `createTaxSummaryMaterializedReportHost(async () =>
|
|
8
|
+
* (await runtime.getCollection('TaxSummaryReport')).db)`.
|
|
9
|
+
* Browser requests select only one of
|
|
10
|
+
* these ids; they never supply a tenant, report constructor, or database.
|
|
11
|
+
*/
|
|
12
|
+
export const materializedReportHosts: readonly MaterializedReportHost[] = [];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { error } from '@sveltejs/kit';
|
|
2
|
+
import { assertMaterializedReportRead, findMaterializedReportHost } from '@happyvertical/smrt-reports';
|
|
3
|
+
import { materializedReportHosts } from '$lib/server/materialized-reports';
|
|
4
|
+
import type { PageServerLoad } from './$types';
|
|
5
|
+
|
|
6
|
+
function pageValue(value: string | null, fallback: number, maximum: number): number {
|
|
7
|
+
if (value === null) return fallback;
|
|
8
|
+
if (!/^\d+$/.test(value)) error(400, 'Invalid report page');
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
if (!Number.isSafeInteger(parsed) || parsed > maximum) error(400, 'Invalid report page');
|
|
11
|
+
return parsed;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const load: PageServerLoad = async ({ locals, params, url }) => {
|
|
15
|
+
if (!locals.user?.id || !locals.tenantId) error(401, 'Sign in required');
|
|
16
|
+
const host = findMaterializedReportHost(materializedReportHosts, params.id);
|
|
17
|
+
if (!host) error(404, 'Report not found');
|
|
18
|
+
const context = {
|
|
19
|
+
userId: locals.user.id,
|
|
20
|
+
tenantId: locals.tenantId,
|
|
21
|
+
permissions: locals.permissions ?? [],
|
|
22
|
+
};
|
|
23
|
+
try { assertMaterializedReportRead(host, context); } catch { error(403, 'Report unavailable'); }
|
|
24
|
+
const descriptor = await host.descriptor(context);
|
|
25
|
+
const offset = pageValue(url.searchParams.get('offset'), 0, 100_000);
|
|
26
|
+
const limit = pageValue(url.searchParams.get('limit'), 50, 100);
|
|
27
|
+
if (limit < 1) error(400, 'Invalid report page');
|
|
28
|
+
const result = await host.query(context, {
|
|
29
|
+
version: 1, requestId: `materialized:${host.id}`, mode: 'rows',
|
|
30
|
+
projection: [descriptor.identityField, ...descriptor.dataTable.columns.map((column) => column.id)],
|
|
31
|
+
page: { kind: 'offset', offset, limit },
|
|
32
|
+
});
|
|
33
|
+
if (result.page?.kind !== 'offset' || result.total.kind === 'unavailable')
|
|
34
|
+
error(500, 'Report page unavailable');
|
|
35
|
+
return {
|
|
36
|
+
hostId: host.id,
|
|
37
|
+
drilldownColumnIds: host.drilldownColumnIds,
|
|
38
|
+
page: {
|
|
39
|
+
descriptor,
|
|
40
|
+
rows: result.rows,
|
|
41
|
+
totalRows: result.total.value,
|
|
42
|
+
offset: result.page.offset,
|
|
43
|
+
limit: result.page.limit,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
4
|
+
import { MaterializedReportTable } from '@happyvertical/smrt-reports/svelte';
|
|
5
|
+
import MaterializedReportDrilldown from '$lib/components/MaterializedReportDrilldown.svelte';
|
|
6
|
+
import { Alert } from '@happyvertical/smrt-ui/feedback';
|
|
7
|
+
import type { PageProps } from './$types';
|
|
8
|
+
|
|
9
|
+
let { data }: PageProps = $props();
|
|
10
|
+
let details = $state<unknown>(undefined);
|
|
11
|
+
let drilldownError = $state<string | null>(null);
|
|
12
|
+
let loading = $state(false);
|
|
13
|
+
let hydrated = $state(false);
|
|
14
|
+
let requestSequence = 0;
|
|
15
|
+
|
|
16
|
+
onMount(() => {
|
|
17
|
+
hydrated = true;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
async function drilldown(cell: { id: string; columnId: string }): Promise<void> {
|
|
21
|
+
const request = ++requestSequence;
|
|
22
|
+
loading = true;
|
|
23
|
+
drilldownError = null;
|
|
24
|
+
details = undefined;
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(`/reports/materialized/${encodeURIComponent(data.hostId)}/drilldown`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'content-type': 'application/json' },
|
|
29
|
+
body: JSON.stringify(cell),
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) throw new Error('Report drilldown unavailable');
|
|
32
|
+
const responseDetails = await response.json();
|
|
33
|
+
if (request !== requestSequence) return;
|
|
34
|
+
details = responseDetails;
|
|
35
|
+
} catch {
|
|
36
|
+
if (request !== requestSequence) return;
|
|
37
|
+
drilldownError = 'Report drilldown unavailable';
|
|
38
|
+
} finally {
|
|
39
|
+
if (request === requestSequence) loading = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function changePage(page: number): Promise<void> {
|
|
44
|
+
if (!Number.isSafeInteger(page) || page < 1) return;
|
|
45
|
+
requestSequence += 1;
|
|
46
|
+
details = undefined;
|
|
47
|
+
drilldownError = null;
|
|
48
|
+
loading = false;
|
|
49
|
+
const offset = (page - 1) * data.page.limit;
|
|
50
|
+
if (!Number.isSafeInteger(offset)) return;
|
|
51
|
+
const url = new URL(window.location.href);
|
|
52
|
+
url.searchParams.set('offset', String(offset));
|
|
53
|
+
url.searchParams.set('limit', String(data.page.limit));
|
|
54
|
+
await goto(`${url.pathname}?${url.searchParams.toString()}`, {
|
|
55
|
+
keepFocus: true,
|
|
56
|
+
noScroll: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<svelte:head><title>{data.page.descriptor.reportClassName}</title></svelte:head>
|
|
62
|
+
|
|
63
|
+
<main class="page" data-materialized-report-hydrated={hydrated ? 'true' : 'false'}>
|
|
64
|
+
<MaterializedReportTable
|
|
65
|
+
page={{
|
|
66
|
+
...data.page,
|
|
67
|
+
drilldownColumnIds: data.drilldownColumnIds,
|
|
68
|
+
}}
|
|
69
|
+
onDrilldown={drilldown}
|
|
70
|
+
onPageChange={changePage}
|
|
71
|
+
caption={data.page.descriptor.reportClassName}
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
{#if loading}
|
|
75
|
+
<Alert variant="info" title="Loading report details">
|
|
76
|
+
Loading the contributors for the selected amount.
|
|
77
|
+
</Alert>
|
|
78
|
+
{:else if drilldownError}
|
|
79
|
+
<Alert variant="error" title="Report drilldown unavailable">
|
|
80
|
+
{drilldownError}
|
|
81
|
+
</Alert>
|
|
82
|
+
{:else if details}
|
|
83
|
+
<MaterializedReportDrilldown {details} />
|
|
84
|
+
{/if}
|
|
85
|
+
</main>
|
|
86
|
+
|
|
87
|
+
<style>
|
|
88
|
+
@layer smrt.theme {
|
|
89
|
+
.page {
|
|
90
|
+
display: grid;
|
|
91
|
+
gap: var(--smrt-spacing-6);
|
|
92
|
+
width: min(100%, 76rem);
|
|
93
|
+
margin-inline: auto;
|
|
94
|
+
padding: var(--smrt-spacing-6) var(--smrt-spacing-4);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { assertMaterializedReportRead, findMaterializedReportHost } from '@happyvertical/smrt-reports';
|
|
3
|
+
import { materializedReportHosts } from '$lib/server/materialized-reports';
|
|
4
|
+
import type { RequestHandler } from './$types';
|
|
5
|
+
|
|
6
|
+
export const POST: RequestHandler = async ({ request, locals, params }) => {
|
|
7
|
+
if (!locals.user?.id || !locals.tenantId)
|
|
8
|
+
return json({ error: 'Sign in required' }, { status: 401 });
|
|
9
|
+
const host = findMaterializedReportHost(materializedReportHosts, params.id);
|
|
10
|
+
if (!host) return json({ error: 'Report not found' }, { status: 404 });
|
|
11
|
+
const context = { userId: locals.user.id, tenantId: locals.tenantId, permissions: locals.permissions ?? [] };
|
|
12
|
+
try { assertMaterializedReportRead(host, context); } catch { return json({ error: 'Report unavailable' }, { status: 403 }); }
|
|
13
|
+
let body: unknown;
|
|
14
|
+
try { body = await request.json(); } catch { return json({ error: 'Invalid request' }, { status: 400 }); }
|
|
15
|
+
if (!body || typeof body !== 'object') return json({ error: 'Invalid request' }, { status: 400 });
|
|
16
|
+
const { id, columnId } = body as Record<string, unknown>;
|
|
17
|
+
if (typeof id !== 'string' || !id || typeof columnId !== 'string' || !columnId)
|
|
18
|
+
return json({ error: 'Invalid request' }, { status: 400 });
|
|
19
|
+
try {
|
|
20
|
+
return json(await host.drilldown(context, { id, columnId }));
|
|
21
|
+
} catch {
|
|
22
|
+
return json({ error: 'Report drilldown unavailable' }, { status: 404 });
|
|
23
|
+
}
|
|
24
|
+
};
|
package/template.config.js
CHANGED
|
@@ -19,23 +19,23 @@ export default {
|
|
|
19
19
|
|
|
20
20
|
// Dependencies to add to generated project
|
|
21
21
|
dependencies: {
|
|
22
|
-
'@happyvertical/smrt-app-runtime': '^0.56.
|
|
23
|
-
'@happyvertical/smrt-config': '^0.56.
|
|
24
|
-
'@happyvertical/smrt-core': '^0.56.
|
|
25
|
-
'@happyvertical/smrt-jobs': '^0.56.
|
|
26
|
-
'@happyvertical/smrt-profiles': '^0.56.
|
|
27
|
-
'@happyvertical/smrt-reports': '^0.56.
|
|
28
|
-
'@happyvertical/smrt-svelte': '^0.56.
|
|
29
|
-
'@happyvertical/smrt-tenancy': '^0.56.
|
|
30
|
-
'@happyvertical/smrt-ui': '^0.56.
|
|
31
|
-
'@happyvertical/smrt-users': '^0.56.
|
|
32
|
-
'@happyvertical/smrt-web': '^0.56.
|
|
22
|
+
'@happyvertical/smrt-app-runtime': '^0.56.2',
|
|
23
|
+
'@happyvertical/smrt-config': '^0.56.2',
|
|
24
|
+
'@happyvertical/smrt-core': '^0.56.2',
|
|
25
|
+
'@happyvertical/smrt-jobs': '^0.56.2',
|
|
26
|
+
'@happyvertical/smrt-profiles': '^0.56.2',
|
|
27
|
+
'@happyvertical/smrt-reports': '^0.56.2',
|
|
28
|
+
'@happyvertical/smrt-svelte': '^0.56.2',
|
|
29
|
+
'@happyvertical/smrt-tenancy': '^0.56.2',
|
|
30
|
+
'@happyvertical/smrt-ui': '^0.56.2',
|
|
31
|
+
'@happyvertical/smrt-users': '^0.56.2',
|
|
32
|
+
'@happyvertical/smrt-web': '^0.56.2',
|
|
33
33
|
'@happyvertical/sql': '^0.89.12',
|
|
34
34
|
'@modelcontextprotocol/server': '^2.0.0',
|
|
35
35
|
},
|
|
36
36
|
|
|
37
37
|
devDependencies: {
|
|
38
|
-
'@happyvertical/smrt-cli': '^0.56.
|
|
38
|
+
'@happyvertical/smrt-cli': '^0.56.2',
|
|
39
39
|
'@sveltejs/adapter-node': '^5.5.7',
|
|
40
40
|
'@sveltejs/kit': '2.70.3',
|
|
41
41
|
'@sveltejs/vite-plugin-svelte': '^7.3.0',
|