angular-my-report 0.0.0-stage → 0.1.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.
Files changed (46) hide show
  1. package/README.md +99 -2
  2. package/ng-package.json +7 -0
  3. package/package.json +23 -3
  4. package/src/lib/engine/barcode.ts +118 -0
  5. package/src/lib/engine/chart.ts +85 -0
  6. package/src/lib/engine/chartLayout.ts +265 -0
  7. package/src/lib/engine/crosstab.ts +96 -0
  8. package/src/lib/engine/dataSources.ts +204 -0
  9. package/src/lib/engine/expressions.ts +734 -0
  10. package/src/lib/engine/measure.ts +62 -0
  11. package/src/lib/engine/parameters.ts +96 -0
  12. package/src/lib/engine/qr.ts +26 -0
  13. package/src/lib/engine/renderer.ts +324 -0
  14. package/src/lib/engine/table.ts +58 -0
  15. package/src/lib/model/defaults.ts +249 -0
  16. package/src/lib/model/factories.ts +324 -0
  17. package/src/lib/model/types.ts +281 -0
  18. package/src/lib/ui/css.ts +12 -0
  19. package/src/lib/ui/designer/banded-table-dialog.component.ts +127 -0
  20. package/src/lib/ui/designer/canvas.component.ts +281 -0
  21. package/src/lib/ui/designer/data-source-dialog.component.ts +201 -0
  22. package/src/lib/ui/designer/element-box.component.ts +138 -0
  23. package/src/lib/ui/designer/graphics.components.ts +140 -0
  24. package/src/lib/ui/designer/left-panel.component.ts +189 -0
  25. package/src/lib/ui/designer/safe-image.pipe.ts +16 -0
  26. package/src/lib/ui/designer/table-view.component.ts +120 -0
  27. package/src/lib/ui/designer.component.ts +296 -0
  28. package/src/lib/ui/designer.store.ts +230 -0
  29. package/src/lib/ui/inputs.ts +34 -0
  30. package/src/lib/ui/myreport.css +267 -0
  31. package/src/lib/ui/properties/band-props.component.ts +86 -0
  32. package/src/lib/ui/properties/element-props.component.ts +326 -0
  33. package/src/lib/ui/properties/expr-input.component.ts +191 -0
  34. package/src/lib/ui/properties/graphics-props.component.ts +260 -0
  35. package/src/lib/ui/properties/image-picker.component.ts +62 -0
  36. package/src/lib/ui/properties/properties-panel.component.ts +37 -0
  37. package/src/lib/ui/properties/report-extras.component.ts +198 -0
  38. package/src/lib/ui/properties/report-props.component.ts +211 -0
  39. package/src/lib/ui/properties/table-props.component.ts +197 -0
  40. package/src/lib/ui/viewer/page-view.component.ts +73 -0
  41. package/src/lib/ui/viewer/render.ts +161 -0
  42. package/src/lib/ui/viewer/viewer.component.ts +555 -0
  43. package/src/lib/ui/viewer.component.ts +107 -0
  44. package/src/public-api.ts +12 -0
  45. package/tsconfig.lib.json +12 -0
  46. package/tsconfig.lib.prod.json +5 -0
@@ -0,0 +1,281 @@
1
+ // Report definition model. This JSON shape is the contract between the
2
+ // designer and any renderer (the browser preview here, or a server engine later).
3
+
4
+ export type BandType =
5
+ | 'reportHeader'
6
+ | 'pageHeader'
7
+ | 'groupHeader'
8
+ | 'detail'
9
+ | 'groupFooter'
10
+ | 'pageFooter'
11
+ | 'reportFooter';
12
+
13
+ export type ElementKind = 'text' | 'field' | 'line' | 'rect' | 'image' | 'table' | 'chart' | 'barcode' | 'qrcode' | 'crosstab';
14
+ export type HAlign = 'left' | 'center' | 'right';
15
+ export type VAlign = 'top' | 'middle' | 'bottom';
16
+
17
+ export interface ElementStyle {
18
+ fontFamily: string;
19
+ fontSize: number;
20
+ bold: boolean;
21
+ italic: boolean;
22
+ underline: boolean;
23
+ color: string;
24
+ background: string; // css color or 'transparent'
25
+ align: HAlign;
26
+ vAlign: VAlign;
27
+ borderWidth: number;
28
+ borderColor: string;
29
+ padding: number;
30
+ }
31
+
32
+ export type FormatKind = 'none' | 'number' | 'currency' | 'percent' | 'date' | 'datetime' | 'time';
33
+
34
+ export interface FormatSpec {
35
+ kind: FormatKind;
36
+ decimals: number;
37
+ currency: string;
38
+ }
39
+
40
+ export interface ReportElement {
41
+ id: string;
42
+ kind: ElementKind;
43
+ x: number;
44
+ y: number;
45
+ w: number;
46
+ h: number;
47
+ /** Template text. `{Field}`, `{sum(Amount)}`, `{PageNumber}`… For images: the URL (may contain {Field}). */
48
+ text: string;
49
+ format: FormatSpec;
50
+ style: ElementStyle;
51
+ /** Text/field grows taller to fit its content (pushing elements below down). Default true. */
52
+ canGrow?: boolean;
53
+ /** Only for kind 'table'. */
54
+ table?: TableSpec;
55
+ /** Only for kind 'chart'. */
56
+ chart?: ChartSpec;
57
+ /** Only for kind 'barcode' (the value is the `text` template). */
58
+ barcode?: BarcodeSpec;
59
+ /** Only for kind 'qrcode' (the value is the `text` template). */
60
+ qr?: QrSpec;
61
+ /** Only for kind 'crosstab'. */
62
+ crosstab?: CrossTabSpec;
63
+ /** Expression; the element is hidden when it evaluates to false, e.g. "Amount > 100". */
64
+ visibleWhen?: string;
65
+ /** Extra options for labels and fields. */
66
+ fieldOptions?: FieldOptions;
67
+ /** Lines and rectangles: stroke style. */
68
+ dash?: 'solid' | 'dashed' | 'dotted';
69
+ /** Images: how the picture fits its box. */
70
+ imageFit?: 'contain' | 'cover' | 'fill';
71
+ }
72
+
73
+ export interface FieldOptions {
74
+ /** Shown when the text evaluates to empty. */
75
+ nullText: string;
76
+ /** In repeating bands, leave the box empty when the value equals the previous row's (per page). */
77
+ hideDuplicates: boolean;
78
+ /** Conditional formatting: first matching rule wins. */
79
+ rules: StyleRule[];
80
+ }
81
+
82
+ export interface StyleRule {
83
+ /** Expression, e.g. "Amount > 1000". */
84
+ when: string;
85
+ color: string;
86
+ /** '' = unchanged */
87
+ background: string;
88
+ bold: boolean;
89
+ }
90
+
91
+ export interface TableColumn {
92
+ id: string;
93
+ header: string;
94
+ /** Cell template evaluated per row, e.g. "{Amount}". */
95
+ text: string;
96
+ /** Optional footer (totals) template, e.g. "{sum(Amount)}". */
97
+ footer: string;
98
+ /** Relative width; columns share the table width in proportion. */
99
+ width: number;
100
+ align: HAlign;
101
+ format: FormatSpec;
102
+ }
103
+
104
+ /**
105
+ * A table lists the rows in its band's scope: all rows in Report Header/Footer, the group's
106
+ * rows in Group Header/Footer, the current record in Detail. It flows across pages.
107
+ */
108
+ export interface TableSpec {
109
+ columns: TableColumn[];
110
+ showHeader: boolean;
111
+ repeatHeader: boolean;
112
+ showFooter: boolean;
113
+ headerHeight: number;
114
+ rowHeight: number;
115
+ headerBackground: string;
116
+ headerColor: string;
117
+ /** Alternate-row background or 'transparent'. */
118
+ stripeBackground: string;
119
+ /** Grid line color or 'transparent'. */
120
+ gridColor: string;
121
+ }
122
+
123
+ export interface Band {
124
+ id: string;
125
+ type: BandType;
126
+ height: number;
127
+ visible: boolean;
128
+ groupId?: string;
129
+ elements: ReportElement[];
130
+ }
131
+
132
+ export interface Group {
133
+ id: string;
134
+ field: string;
135
+ direction: 'asc' | 'desc';
136
+ }
137
+
138
+ export interface SortSpec {
139
+ field: string;
140
+ direction: 'asc' | 'desc';
141
+ }
142
+
143
+ export interface PageSettings {
144
+ width: number; // px @ 96dpi
145
+ height: number;
146
+ marginTop: number;
147
+ marginBottom: number;
148
+ marginLeft: number;
149
+ marginRight: number;
150
+ /** Receipt roll: one page as long as its content, no page breaks; the page footer follows the content. */
151
+ continuous?: boolean;
152
+ }
153
+
154
+ export type DataSourceType = 'json' | 'odata';
155
+
156
+ export interface DataSourceDef {
157
+ type: DataSourceType;
158
+ /** JSON: file/API URL. OData: service root URL (or full entity-set URL). */
159
+ url: string;
160
+ /** JSON only: pasted JSON used instead of the URL when non-empty. */
161
+ inline: string;
162
+ /** JSON only: dot path to the array inside the response, e.g. "data.items". */
163
+ dataPath: string;
164
+ /** Extra request headers, one "Name: value" per line. */
165
+ headers: string;
166
+ // OData query options
167
+ entitySet: string;
168
+ select: string;
169
+ filter: string;
170
+ orderby: string;
171
+ expand: string;
172
+ top: number | null;
173
+ /** Safety cap when following OData nextLink paging. */
174
+ maxRows: number;
175
+ }
176
+
177
+ export interface ReportDefinition {
178
+ version: 1;
179
+ name: string;
180
+ page: PageSettings;
181
+ dataSource: DataSourceDef;
182
+ groups: Group[];
183
+ sort: SortSpec[];
184
+ bands: Band[];
185
+ /** Values the user supplies at run time; used as @Name in formulas and {@Name} in the data source. */
186
+ parameters?: ReportParameter[];
187
+ /** Expression applied to every data row; rows where it is false are left out, e.g. "Country == @Country". */
188
+ filter?: string;
189
+ watermark?: Watermark;
190
+ background?: PageBackground;
191
+ }
192
+
193
+ export type ParameterType = 'string' | 'number' | 'date' | 'boolean';
194
+
195
+ export interface ReportParameter {
196
+ name: string;
197
+ label: string;
198
+ type: ParameterType;
199
+ defaultValue: string;
200
+ /** Comma-separated choices; empty = free input. */
201
+ options: string;
202
+ }
203
+
204
+ export interface Watermark {
205
+ enabled: boolean;
206
+ kind: 'text' | 'image';
207
+ text: string;
208
+ /** URL or data: URL. */
209
+ image: string;
210
+ /** 0–1 */
211
+ opacity: number;
212
+ /** Degrees, e.g. -45. */
213
+ angle: number;
214
+ fontSize: number;
215
+ color: string;
216
+ bold: boolean;
217
+ /** Image width as % of the page width. */
218
+ imageScale: number;
219
+ layer: 'behind' | 'front';
220
+ pages: 'all' | 'first';
221
+ }
222
+
223
+ export interface PageBackground {
224
+ enabled: boolean;
225
+ /** URL or data: URL. */
226
+ image: string;
227
+ fit: 'cover' | 'contain' | 'stretch' | 'center';
228
+ pages: 'all' | 'first';
229
+ }
230
+
231
+ export type AggregateKind = 'sum' | 'count' | 'avg' | 'min' | 'max';
232
+ export type ChartType = 'bar' | 'hbar' | 'line' | 'area' | 'pie' | 'doughnut';
233
+
234
+ /** A chart over the rows in its band's scope, grouped by a category field. */
235
+ export interface ChartSpec {
236
+ type: ChartType;
237
+ title: string;
238
+ categoryField: string;
239
+ /** One series per value field (pie/doughnut use the first). */
240
+ valueFields: string[];
241
+ aggregate: AggregateKind;
242
+ sort: 'none' | 'category' | 'valueDesc' | 'valueAsc';
243
+ /** Extra categories fold into "Other". */
244
+ maxCategories: number;
245
+ showLegend: boolean;
246
+ showValues: boolean;
247
+ showGrid: boolean;
248
+ valueFormat: FormatSpec;
249
+ }
250
+
251
+ export type BarcodeSymbology = 'code128' | 'ean13';
252
+
253
+ export interface BarcodeSpec {
254
+ symbology: BarcodeSymbology;
255
+ showText: boolean;
256
+ color: string;
257
+ }
258
+
259
+ export interface QrSpec {
260
+ errorCorrection: 'L' | 'M' | 'Q' | 'H';
261
+ color: string;
262
+ }
263
+
264
+ /** Pivot: one row per rowField value, one column per columnField value, cells = aggregate(valueField). */
265
+ export interface CrossTabSpec {
266
+ rowField: string;
267
+ columnField: string;
268
+ valueField: string;
269
+ aggregate: AggregateKind;
270
+ cornerText: string;
271
+ showRowTotals: boolean;
272
+ showColumnTotals: boolean;
273
+ valueFormat: FormatSpec;
274
+ headerHeight: number;
275
+ rowHeight: number;
276
+ headerBackground: string;
277
+ headerColor: string;
278
+ stripeBackground: string;
279
+ gridColor: string;
280
+ repeatHeader: boolean;
281
+ }
@@ -0,0 +1,12 @@
1
+ const UNITLESS = new Set(['fontWeight', 'lineHeight', 'flex', 'zIndex', 'opacity']);
2
+
3
+ /** Builds an inline style string from a camelCase map; numbers get "px" unless unitless. */
4
+ export function css(o: Record<string, string | number | undefined | null | false>): string {
5
+ let s = '';
6
+ for (const [k, v] of Object.entries(o)) {
7
+ if (v == null || v === '' || v === false) continue;
8
+ const key = k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
9
+ s += `${key}:${typeof v === 'number' && !UNITLESS.has(k) ? `${v}px` : v};`;
10
+ }
11
+ return s;
12
+ }
@@ -0,0 +1,127 @@
1
+ import { ChangeDetectionStrategy, Component, computed, inject, output, signal } from '@angular/core';
2
+ import { FormsModule } from '@angular/forms';
3
+ import { addBandedTable, type BandedTableOptions } from '../../model/factories';
4
+ import { DesignerStore } from '../designer.store';
5
+
6
+ /**
7
+ * Header/Detail table wizard: titles go in a header band, one field per column in Detail.
8
+ * Everything it creates is ordinary labels/fields, editable afterwards.
9
+ */
10
+ @Component({
11
+ selector: 'rpt-banded-table-dialog',
12
+ standalone: true,
13
+ imports: [FormsModule],
14
+ changeDetection: ChangeDetectionStrategy.OnPush,
15
+ template: `
16
+ <div class="modal-backdrop" (pointerdown)="onBackdropDown($event)">
17
+ <div class="modal">
18
+ <header>
19
+ <h2>Header / Detail table</h2>
20
+ <button class="link" (click)="closed.emit()">✕</button>
21
+ </header>
22
+ <div class="modal-body">
23
+ @if (!fields().length) {
24
+ <p class="error">Load a data source first — the columns are made from its fields.</p>
25
+ } @else {
26
+ <div class="field">
27
+ Columns ({{ selectedCount() }} selected)
28
+ <div class="wizard-fields">
29
+ @for (f of fields(); track f.name) {
30
+ <label class="check">
31
+ <input type="checkbox" [ngModel]="picked()[f.name]" (ngModelChange)="toggle(f.name, $event)" />
32
+ {{ f.name }} <span class="muted">({{ f.type }})</span>
33
+ </label>
34
+ }
35
+ </div>
36
+ <div class="inline">
37
+ <button class="small" (click)="all(true)">All</button>
38
+ <button class="small" (click)="all(false)">None</button>
39
+ </div>
40
+ </div>
41
+ <div class="grid2">
42
+ <label class="field">
43
+ Column titles in
44
+ <select [(ngModel)]="titleBand">
45
+ <option value="pageHeader">Page Header (every page)</option>
46
+ <option value="reportHeader">Report Header (first page)</option>
47
+ @if (hasGroups()) {
48
+ <option value="groupHeader">Group Header (every group)</option>
49
+ }
50
+ </select>
51
+ </label>
52
+ <label class="field">
53
+ Row height
54
+ <input type="number" min="12" [(ngModel)]="rowHeight" />
55
+ </label>
56
+ <label class="field">
57
+ Title background
58
+ <input type="color" [(ngModel)]="headerBackground" />
59
+ </label>
60
+ <label class="field">
61
+ Title text
62
+ <input type="color" [(ngModel)]="headerColor" />
63
+ </label>
64
+ </div>
65
+ <label class="check"><input type="checkbox" [(ngModel)]="grid" /> Grid borders around cells</label>
66
+ <label class="check"><input type="checkbox" [(ngModel)]="totals" /> Totals of number columns in Report Footer</label>
67
+ <label class="check"><input type="checkbox" [(ngModel)]="replace" /> Replace what is in the title band and Detail band now</label>
68
+ }
69
+ </div>
70
+ <footer>
71
+ <button (click)="closed.emit()">Cancel</button>
72
+ <button class="primary" [disabled]="!selectedCount()" (click)="create()">Create table</button>
73
+ </footer>
74
+ </div>
75
+ </div>
76
+ `,
77
+ })
78
+ export class BandedTableDialogComponent {
79
+ private readonly store = inject(DesignerStore);
80
+ readonly closed = output<void>();
81
+
82
+ protected readonly fields = computed(() => this.store.data().fields.filter((f) => f.type !== 'object'));
83
+ protected readonly hasGroups = computed(() => this.store.report().groups.length > 0);
84
+ protected readonly picked = signal<Record<string, boolean>>(
85
+ Object.fromEntries(this.store.data().fields.filter((f) => f.type !== 'object').map((f, i) => [f.name, i < 8])),
86
+ );
87
+ protected readonly selectedCount = computed(() => this.fields().filter((f) => this.picked()[f.name]).length);
88
+
89
+ protected titleBand: BandedTableOptions['titleBand'] = 'pageHeader';
90
+ protected rowHeight = 22;
91
+ protected headerBackground = '#1f3a68';
92
+ protected headerColor = '#ffffff';
93
+ protected grid = true;
94
+ protected totals = true;
95
+ protected replace = true;
96
+
97
+ protected onBackdropDown(e: PointerEvent): void {
98
+ if (e.target === e.currentTarget) this.closed.emit();
99
+ }
100
+
101
+ protected toggle(name: string, on: boolean): void {
102
+ this.picked.update((p) => ({ ...p, [name]: on }));
103
+ }
104
+
105
+ protected all(on: boolean): void {
106
+ this.picked.set(Object.fromEntries(this.fields().map((f) => [f.name, on])));
107
+ }
108
+
109
+ protected create(): void {
110
+ const opts: BandedTableOptions = {
111
+ fields: this.fields().filter((f) => this.picked()[f.name]),
112
+ titleBand: this.titleBand,
113
+ grid: this.grid,
114
+ headerBackground: this.headerBackground,
115
+ headerColor: this.headerColor,
116
+ totals: this.totals,
117
+ rowHeight: Math.max(12, Number(this.rowHeight) || 22),
118
+ replace: this.replace,
119
+ };
120
+ let ids: string[] = [];
121
+ this.store.update((r) => (ids = addBandedTable(r, opts)));
122
+ const detail = this.store.report().bands.find((b) => b.type === 'detail');
123
+ this.store.select({ elementIds: [], bandId: detail?.id ?? null });
124
+ void ids;
125
+ this.closed.emit();
126
+ }
127
+ }