@isaiahiroko/tilmart-ngx-apps-admin 0.2.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 (75) hide show
  1. package/README.md +9 -0
  2. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-articles.module-OeLRnPwN.mjs +154 -0
  3. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-articles.module-OeLRnPwN.mjs.map +1 -0
  4. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-audit.module-C9gvw8X2.mjs +84 -0
  5. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-audit.module-C9gvw8X2.mjs.map +1 -0
  6. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-baskets.module-_lHPl1dT.mjs +294 -0
  7. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-baskets.module-_lHPl1dT.mjs.map +1 -0
  8. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-catalog.module-Bvx2tVOi.mjs +224 -0
  9. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-catalog.module-Bvx2tVOi.mjs.map +1 -0
  10. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-claims.module-Ccsd8Yx3.mjs +106 -0
  11. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-claims.module-Ccsd8Yx3.mjs.map +1 -0
  12. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-compatibility-rules.module-xN-JUWwp.mjs +261 -0
  13. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-compatibility-rules.module-xN-JUWwp.mjs.map +1 -0
  14. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-content.module-DLdKAUqc.mjs +365 -0
  15. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-content.module-DLdKAUqc.mjs.map +1 -0
  16. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-coupons.module-CQNCSlsd.mjs +106 -0
  17. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-coupons.module-CQNCSlsd.mjs.map +1 -0
  18. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-customers.module-12XdV9zo.mjs +106 -0
  19. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-customers.module-12XdV9zo.mjs.map +1 -0
  20. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-data-quality.module-Bfgh3zE5.mjs +286 -0
  21. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-data-quality.module-Bfgh3zE5.mjs.map +1 -0
  22. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-data-requests.module-C6tPNlup.mjs +104 -0
  23. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-data-requests.module-C6tPNlup.mjs.map +1 -0
  24. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-disputes.module-DmUoAS0Z.mjs +73 -0
  25. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-disputes.module-DmUoAS0Z.mjs.map +1 -0
  26. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-homepage.module-BZ3UmxE3.mjs +404 -0
  27. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-homepage.module-BZ3UmxE3.mjs.map +1 -0
  28. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-inbox.module-BZDTa1L-.mjs +290 -0
  29. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-inbox.module-BZDTa1L-.mjs.map +1 -0
  30. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-macros.module-BO4bdER2.mjs +112 -0
  31. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-macros.module-BO4bdER2.mjs.map +1 -0
  32. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-moderation.module-CZOCrias.mjs +116 -0
  33. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-moderation.module-CZOCrias.mjs.map +1 -0
  34. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-navigation.module-DoKzzRgn.mjs +281 -0
  35. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-navigation.module-DoKzzRgn.mjs.map +1 -0
  36. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-order-board.module-DIOjhptn.mjs +115 -0
  37. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-order-board.module-DIOjhptn.mjs.map +1 -0
  38. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-orders.module-Ddw49QW0.mjs +185 -0
  39. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-orders.module-Ddw49QW0.mjs.map +1 -0
  40. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-payments.module-BhqAtTZs.mjs +106 -0
  41. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-payments.module-BhqAtTZs.mjs.map +1 -0
  42. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-payouts.module-CoiscPIZ.mjs +70 -0
  43. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-payouts.module-CoiscPIZ.mjs.map +1 -0
  44. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-procurement.module-qXW-g0pf.mjs +274 -0
  45. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-procurement.module-qXW-g0pf.mjs.map +1 -0
  46. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-promotions.module-DG9uTDqX.mjs +106 -0
  47. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-promotions.module-DG9uTDqX.mjs.map +1 -0
  48. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-queues.module-K-zYYVFy.mjs +84 -0
  49. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-queues.module-K-zYYVFy.mjs.map +1 -0
  50. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-redirects.module-Hm2dO_bC.mjs +106 -0
  51. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-redirects.module-Hm2dO_bC.mjs.map +1 -0
  52. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-referrals.module-QLNhuh7q.mjs +106 -0
  53. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-referrals.module-QLNhuh7q.mjs.map +1 -0
  54. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-reports.module-DBZov0We.mjs +104 -0
  55. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-reports.module-DBZov0We.mjs.map +1 -0
  56. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-returns.module-Bd2gdo6_.mjs +106 -0
  57. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-returns.module-Bd2gdo6_.mjs.map +1 -0
  58. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-rich-editor.module-DW9Gy_QQ.mjs +81 -0
  59. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-rich-editor.module-DW9Gy_QQ.mjs.map +1 -0
  60. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-routes.module-BkVY06ci.mjs +105 -0
  61. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-routes.module-BkVY06ci.mjs.map +1 -0
  62. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-settings.module-DKeZTldO.mjs +222 -0
  63. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-settings.module-DKeZTldO.mjs.map +1 -0
  64. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-shipments.module-XuaOKr8v.mjs +150 -0
  65. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-shipments.module-XuaOKr8v.mjs.map +1 -0
  66. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-stock.module-DYiAkF7E.mjs +107 -0
  67. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-stock.module-DYiAkF7E.mjs.map +1 -0
  68. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-trace.module-Pwr-82Rc.mjs +92 -0
  69. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-trace.module-Pwr-82Rc.mjs.map +1 -0
  70. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-users.module-Bx_7fI2g.mjs +111 -0
  71. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin-users.module-Bx_7fI2g.mjs.map +1 -0
  72. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin.mjs +475 -0
  73. package/fesm2022/isaiahiroko-tilmart-ngx-apps-admin.mjs.map +1 -0
  74. package/package.json +34 -0
  75. package/types/isaiahiroko-tilmart-ngx-apps-admin.d.ts +32 -0
@@ -0,0 +1,274 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ChangeDetectorRef, Component, NgModule } from '@angular/core';
3
+ import * as i1$2 from '@angular/router';
4
+ import { RouterModule } from '@angular/router';
5
+ import { BaseDirective, using, getEnv, notify, SharedModule } from '@thescaffold/ngx-core';
6
+ import * as i1$1 from '@thescaffold/ngx-ui';
7
+ import { ElementsModule, WebgetsModule } from '@thescaffold/ngx-ui';
8
+ import { HttpClient } from '@angular/common/http';
9
+ import { firstValueFrom, takeUntil } from 'rxjs';
10
+ import * as i1 from '@angular/forms';
11
+
12
+ // Cross-basket aggregation, not a per-entity CRUD list (docs/PLAN_2.md
13
+ // Phase 11): merges every committed cycle's line items for a shared period
14
+ // into one buy-list, grouped by product, with a done/pending toggle so the
15
+ // ops team can track what's already been sourced. Net-new end to end - no
16
+ // existing UI or backend to build on (confirmed against index/PITCH.md,
17
+ // which has one aspirational sentence about this and nothing else).
18
+ class ProcurementComponent extends BaseDirective {
19
+ constructor() {
20
+ super(...arguments);
21
+ this.http = inject(HttpClient);
22
+ this.cdr = inject(ChangeDetectorRef);
23
+ this.periods = [];
24
+ this.periodStart = null;
25
+ this.rows = [];
26
+ this.loading = true;
27
+ this.exporting = false;
28
+ this.init = () => {
29
+ this.bootstrap();
30
+ };
31
+ }
32
+ async bootstrap() {
33
+ try {
34
+ const res = await firstValueFrom(this.apiService.get('apps/grocery/procurement/periods'));
35
+ this.periods = res?.data ?? [];
36
+ this.periodStart = this.periods[0] ?? null;
37
+ if (this.periodStart) {
38
+ await this.load();
39
+ }
40
+ else {
41
+ this.loading = false;
42
+ }
43
+ }
44
+ catch {
45
+ this.loading = false;
46
+ }
47
+ this.cdr.markForCheck();
48
+ }
49
+ async onPeriodChange(periodStart) {
50
+ this.periodStart = periodStart;
51
+ await this.load();
52
+ this.cdr.markForCheck();
53
+ }
54
+ async load() {
55
+ if (!this.periodStart) {
56
+ return;
57
+ }
58
+ this.loading = true;
59
+ this.cdr.markForCheck();
60
+ try {
61
+ const res = await firstValueFrom(this.apiService.get('apps/grocery/procurement', {
62
+ periodStart: this.periodStart,
63
+ }));
64
+ this.rows = res?.data ?? [];
65
+ }
66
+ finally {
67
+ this.loading = false;
68
+ this.cdr.markForCheck();
69
+ }
70
+ }
71
+ toggle(row) {
72
+ this.apiService
73
+ .patch('apps/grocery/procurement/toggle', {
74
+ periodStart: this.periodStart,
75
+ productId: row.productId,
76
+ })
77
+ .pipe(takeUntil(this.$))
78
+ .subscribe(using(({ data }) => {
79
+ row.status = data.status;
80
+ this.cdr.markForCheck();
81
+ }));
82
+ }
83
+ print() {
84
+ window.print();
85
+ }
86
+ // Bypasses ApiService/RequestService: that wrapper always expects the
87
+ // {status,title,message,data} JSON envelope and throws "Network Error"
88
+ // on anything else, so a raw binary file response can't go through it.
89
+ // The global JwtInterceptor still attaches auth headers to this request
90
+ // regardless, since it intercepts every HttpClient call, not just ones
91
+ // made through ApiService.
92
+ async exportExcel() {
93
+ if (!this.periodStart || this.exporting) {
94
+ return;
95
+ }
96
+ this.exporting = true;
97
+ this.cdr.markForCheck();
98
+ try {
99
+ const base = getEnv('API_BASE_URL');
100
+ const blob = await firstValueFrom(this.http.get(`${base}/apps/grocery/procurement/export?periodStart=${this.periodStart}`, { responseType: 'blob' }));
101
+ const url = URL.createObjectURL(blob);
102
+ const link = document.createElement('a');
103
+ link.href = url;
104
+ link.download = `procurement-${this.periodStart}.xlsx`;
105
+ link.click();
106
+ URL.revokeObjectURL(url);
107
+ }
108
+ catch {
109
+ notify('Could not export this period. Please try again.');
110
+ }
111
+ finally {
112
+ this.exporting = false;
113
+ this.cdr.markForCheck();
114
+ }
115
+ }
116
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: ProcurementComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
117
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: ProcurementComponent, isStandalone: false, selector: "x-procurement", usesInheritance: true, ngImport: i0, template: `
118
+ <div class="x-page-head x-no-print">
119
+ <div class="x-page-title">Procurement</div>
120
+ <div class="x-page-subtitle">
121
+ What to buy for a period, aggregated across every committed basket.
122
+ </div>
123
+ </div>
124
+
125
+ <div class="x-toolbar x-no-print">
126
+ <select class="form-select x-period-select" [ngModel]="periodStart" (ngModelChange)="onPeriodChange($event)">
127
+ @if (!periods.length) {
128
+ <option [ngValue]="null">No periods yet</option>
129
+ }
130
+ @for (p of periods; track p) {
131
+ <option [value]="p">{{ p }}</option>
132
+ }
133
+ </select>
134
+ <button type="button" class="btn btn-outline-secondary" (click)="print()">Print</button>
135
+ <button type="button" class="btn btn-success" [disabled]="!periodStart || exporting" (click)="exportExcel()">
136
+ {{ exporting ? 'Exporting…' : 'Export to Excel' }}
137
+ </button>
138
+ </div>
139
+
140
+ <div class="x-print-header">
141
+ <div class="x-print-title">Procurement list — {{ periodStart }}</div>
142
+ </div>
143
+
144
+ @if (loading) {
145
+ <x-element-loader></x-element-loader>
146
+ } @else if (!rows.length) {
147
+ <p class="text-secondary">Nothing to procure for this period.</p>
148
+ } @else {
149
+ <table class="table">
150
+ <thead>
151
+ <tr>
152
+ <th>Product</th>
153
+ <th>Category</th>
154
+ <th>Unit</th>
155
+ <th>Quantity</th>
156
+ <th>Status</th>
157
+ <th class="x-no-print"></th>
158
+ </tr>
159
+ </thead>
160
+ <tbody>
161
+ @for (row of rows; track row.productId) {
162
+ <tr [class.x-row-done]="row.status === 'done'">
163
+ <td>{{ row.productName }}</td>
164
+ <td>{{ row.category }}</td>
165
+ <td>{{ row.unit }}</td>
166
+ <td>{{ row.qty }}</td>
167
+ <td>
168
+ <span class="badge text-bg-light border">{{ row.status }}</span>
169
+ </td>
170
+ <td class="x-no-print">
171
+ <button type="button" class="btn btn-sm btn-outline-secondary" (click)="toggle(row)">
172
+ Mark {{ row.status === 'done' ? 'pending' : 'done' }}
173
+ </button>
174
+ </td>
175
+ </tr>
176
+ }
177
+ </tbody>
178
+ </table>
179
+ }
180
+ `, isInline: true, styles: [".x-page-head{margin-bottom:20px}.x-page-title{font-size:24px;font-weight:800;color:var(--color-text)}.x-page-subtitle{font-size:13.5px;color:var(--color-text-muted);margin-top:2px}.x-toolbar{display:flex;gap:12px;align-items:center;margin-bottom:20px}.x-period-select{max-width:220px}.x-row-done{opacity:.55}.x-print-header{display:none}@media print{.x-no-print{display:none!important}.x-print-header{display:block;margin-bottom:16px}.x-print-title{font-size:18px;font-weight:800}}\n"], dependencies: [{ kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i1$1.LoaderComponent, selector: "x-element-loader", inputs: ["data"] }] }); }
181
+ }
182
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: ProcurementComponent, decorators: [{
183
+ type: Component,
184
+ args: [{ standalone: false, selector: 'x-procurement', template: `
185
+ <div class="x-page-head x-no-print">
186
+ <div class="x-page-title">Procurement</div>
187
+ <div class="x-page-subtitle">
188
+ What to buy for a period, aggregated across every committed basket.
189
+ </div>
190
+ </div>
191
+
192
+ <div class="x-toolbar x-no-print">
193
+ <select class="form-select x-period-select" [ngModel]="periodStart" (ngModelChange)="onPeriodChange($event)">
194
+ @if (!periods.length) {
195
+ <option [ngValue]="null">No periods yet</option>
196
+ }
197
+ @for (p of periods; track p) {
198
+ <option [value]="p">{{ p }}</option>
199
+ }
200
+ </select>
201
+ <button type="button" class="btn btn-outline-secondary" (click)="print()">Print</button>
202
+ <button type="button" class="btn btn-success" [disabled]="!periodStart || exporting" (click)="exportExcel()">
203
+ {{ exporting ? 'Exporting…' : 'Export to Excel' }}
204
+ </button>
205
+ </div>
206
+
207
+ <div class="x-print-header">
208
+ <div class="x-print-title">Procurement list — {{ periodStart }}</div>
209
+ </div>
210
+
211
+ @if (loading) {
212
+ <x-element-loader></x-element-loader>
213
+ } @else if (!rows.length) {
214
+ <p class="text-secondary">Nothing to procure for this period.</p>
215
+ } @else {
216
+ <table class="table">
217
+ <thead>
218
+ <tr>
219
+ <th>Product</th>
220
+ <th>Category</th>
221
+ <th>Unit</th>
222
+ <th>Quantity</th>
223
+ <th>Status</th>
224
+ <th class="x-no-print"></th>
225
+ </tr>
226
+ </thead>
227
+ <tbody>
228
+ @for (row of rows; track row.productId) {
229
+ <tr [class.x-row-done]="row.status === 'done'">
230
+ <td>{{ row.productName }}</td>
231
+ <td>{{ row.category }}</td>
232
+ <td>{{ row.unit }}</td>
233
+ <td>{{ row.qty }}</td>
234
+ <td>
235
+ <span class="badge text-bg-light border">{{ row.status }}</span>
236
+ </td>
237
+ <td class="x-no-print">
238
+ <button type="button" class="btn btn-sm btn-outline-secondary" (click)="toggle(row)">
239
+ Mark {{ row.status === 'done' ? 'pending' : 'done' }}
240
+ </button>
241
+ </td>
242
+ </tr>
243
+ }
244
+ </tbody>
245
+ </table>
246
+ }
247
+ `, styles: [".x-page-head{margin-bottom:20px}.x-page-title{font-size:24px;font-weight:800;color:var(--color-text)}.x-page-subtitle{font-size:13.5px;color:var(--color-text-muted);margin-top:2px}.x-toolbar{display:flex;gap:12px;align-items:center;margin-bottom:20px}.x-period-select{max-width:220px}.x-row-done{opacity:.55}.x-print-header{display:none}@media print{.x-no-print{display:none!important}.x-print-header{display:block;margin-bottom:16px}.x-print-title{font-size:18px;font-weight:800}}\n"] }]
248
+ }] });
249
+
250
+ class ProcurementModule {
251
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: ProcurementModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
252
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: ProcurementModule, declarations: [ProcurementComponent], imports: [SharedModule,
253
+ ElementsModule,
254
+ WebgetsModule, i1$2.RouterModule] }); }
255
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: ProcurementModule, imports: [SharedModule,
256
+ ElementsModule,
257
+ WebgetsModule,
258
+ RouterModule.forChild([{ path: '', component: ProcurementComponent }])] }); }
259
+ }
260
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: ProcurementModule, decorators: [{
261
+ type: NgModule,
262
+ args: [{
263
+ declarations: [ProcurementComponent],
264
+ imports: [
265
+ SharedModule,
266
+ ElementsModule,
267
+ WebgetsModule,
268
+ RouterModule.forChild([{ path: '', component: ProcurementComponent }]),
269
+ ],
270
+ }]
271
+ }] });
272
+
273
+ export { ProcurementModule };
274
+ //# sourceMappingURL=isaiahiroko-tilmart-ngx-apps-admin-procurement.module-qXW-g0pf.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isaiahiroko-tilmart-ngx-apps-admin-procurement.module-qXW-g0pf.mjs","sources":["../../../projects/admin/src/lib/pages/procurement/procurement.component.ts","../../../projects/admin/src/lib/pages/procurement/procurement.module.ts"],"sourcesContent":["import { HttpClient } from '@angular/common/http';\nimport { ChangeDetectorRef, Component, inject } from '@angular/core';\nimport { BaseDirective, getEnv, notify, using } from '@thescaffold/ngx-core';\nimport { firstValueFrom, takeUntil } from 'rxjs';\n\ninterface ProcurementRowVm {\n productId: string;\n productName: string;\n category: string;\n unit: string;\n qty: number;\n status: 'pending' | 'done';\n}\n\n// Cross-basket aggregation, not a per-entity CRUD list (docs/PLAN_2.md\n// Phase 11): merges every committed cycle's line items for a shared period\n// into one buy-list, grouped by product, with a done/pending toggle so the\n// ops team can track what's already been sourced. Net-new end to end - no\n// existing UI or backend to build on (confirmed against index/PITCH.md,\n// which has one aspirational sentence about this and nothing else).\n@Component({\n standalone: false,\n selector: 'x-procurement',\n template: `\n <div class=\"x-page-head x-no-print\">\n <div class=\"x-page-title\">Procurement</div>\n <div class=\"x-page-subtitle\">\n What to buy for a period, aggregated across every committed basket.\n </div>\n </div>\n\n <div class=\"x-toolbar x-no-print\">\n <select class=\"form-select x-period-select\" [ngModel]=\"periodStart\" (ngModelChange)=\"onPeriodChange($event)\">\n @if (!periods.length) {\n <option [ngValue]=\"null\">No periods yet</option>\n }\n @for (p of periods; track p) {\n <option [value]=\"p\">{{ p }}</option>\n }\n </select>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"print()\">Print</button>\n <button type=\"button\" class=\"btn btn-success\" [disabled]=\"!periodStart || exporting\" (click)=\"exportExcel()\">\n {{ exporting ? 'Exporting…' : 'Export to Excel' }}\n </button>\n </div>\n\n <div class=\"x-print-header\">\n <div class=\"x-print-title\">Procurement list — {{ periodStart }}</div>\n </div>\n\n @if (loading) {\n <x-element-loader></x-element-loader>\n } @else if (!rows.length) {\n <p class=\"text-secondary\">Nothing to procure for this period.</p>\n } @else {\n <table class=\"table\">\n <thead>\n <tr>\n <th>Product</th>\n <th>Category</th>\n <th>Unit</th>\n <th>Quantity</th>\n <th>Status</th>\n <th class=\"x-no-print\"></th>\n </tr>\n </thead>\n <tbody>\n @for (row of rows; track row.productId) {\n <tr [class.x-row-done]=\"row.status === 'done'\">\n <td>{{ row.productName }}</td>\n <td>{{ row.category }}</td>\n <td>{{ row.unit }}</td>\n <td>{{ row.qty }}</td>\n <td>\n <span class=\"badge text-bg-light border\">{{ row.status }}</span>\n </td>\n <td class=\"x-no-print\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click)=\"toggle(row)\">\n Mark {{ row.status === 'done' ? 'pending' : 'done' }}\n </button>\n </td>\n </tr>\n }\n </tbody>\n </table>\n }\n `,\n styles: [\n `\n .x-page-head {\n margin-bottom: 20px;\n }\n .x-page-title {\n font-size: 24px;\n font-weight: 800;\n color: var(--color-text);\n }\n .x-page-subtitle {\n font-size: 13.5px;\n color: var(--color-text-muted);\n margin-top: 2px;\n }\n .x-toolbar {\n display: flex;\n gap: 12px;\n align-items: center;\n margin-bottom: 20px;\n }\n .x-period-select {\n max-width: 220px;\n }\n .x-row-done {\n opacity: 0.55;\n }\n .x-print-header {\n display: none;\n }\n\n @media print {\n .x-no-print {\n display: none !important;\n }\n .x-print-header {\n display: block;\n margin-bottom: 16px;\n }\n .x-print-title {\n font-size: 18px;\n font-weight: 800;\n }\n }\n `,\n ],\n})\nexport class ProcurementComponent extends BaseDirective {\n private readonly http = inject(HttpClient);\n private readonly cdr = inject(ChangeDetectorRef);\n\n periods: string[] = [];\n periodStart: string | null = null;\n rows: ProcurementRowVm[] = [];\n loading = true;\n exporting = false;\n\n override init = () => {\n this.bootstrap();\n };\n\n private async bootstrap() {\n try {\n const res: any = await firstValueFrom(\n this.apiService.get('apps/grocery/procurement/periods'),\n );\n this.periods = res?.data ?? [];\n this.periodStart = this.periods[0] ?? null;\n if (this.periodStart) {\n await this.load();\n } else {\n this.loading = false;\n }\n } catch {\n this.loading = false;\n }\n this.cdr.markForCheck();\n }\n\n async onPeriodChange(periodStart: string) {\n this.periodStart = periodStart;\n await this.load();\n this.cdr.markForCheck();\n }\n\n async load() {\n if (!this.periodStart) {\n return;\n }\n this.loading = true;\n this.cdr.markForCheck();\n try {\n const res: any = await firstValueFrom(\n this.apiService.get('apps/grocery/procurement', {\n periodStart: this.periodStart,\n } as any),\n );\n this.rows = res?.data ?? [];\n } finally {\n this.loading = false;\n this.cdr.markForCheck();\n }\n }\n\n toggle(row: ProcurementRowVm) {\n this.apiService\n .patch('apps/grocery/procurement/toggle', {\n periodStart: this.periodStart,\n productId: row.productId,\n })\n .pipe(takeUntil(this.$))\n .subscribe(\n using(({ data }: any) => {\n row.status = data.status;\n this.cdr.markForCheck();\n }),\n );\n }\n\n print() {\n window.print();\n }\n\n // Bypasses ApiService/RequestService: that wrapper always expects the\n // {status,title,message,data} JSON envelope and throws \"Network Error\"\n // on anything else, so a raw binary file response can't go through it.\n // The global JwtInterceptor still attaches auth headers to this request\n // regardless, since it intercepts every HttpClient call, not just ones\n // made through ApiService.\n async exportExcel() {\n if (!this.periodStart || this.exporting) {\n return;\n }\n this.exporting = true;\n this.cdr.markForCheck();\n\n try {\n const base = getEnv('API_BASE_URL');\n const blob = await firstValueFrom(\n this.http.get(\n `${base}/apps/grocery/procurement/export?periodStart=${this.periodStart}`,\n { responseType: 'blob' },\n ),\n );\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `procurement-${this.periodStart}.xlsx`;\n link.click();\n URL.revokeObjectURL(url);\n } catch {\n notify('Could not export this period. Please try again.');\n } finally {\n this.exporting = false;\n this.cdr.markForCheck();\n }\n }\n}\n","import { NgModule } from '@angular/core';\nimport { RouterModule } from '@angular/router';\nimport { SharedModule } from '@thescaffold/ngx-core';\nimport { ElementsModule, WebgetsModule } from '@thescaffold/ngx-ui';\nimport { ProcurementComponent } from './procurement.component';\n\n@NgModule({\n declarations: [ProcurementComponent],\n imports: [\n SharedModule,\n ElementsModule,\n WebgetsModule,\n RouterModule.forChild([{ path: '', component: ProcurementComponent }]),\n ],\n})\nexport class ProcurementModule {}\n"],"names":["i2","i1"],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Component, NgModule } from '@angular/core';
3
+ import * as i1$1 from '@angular/router';
4
+ import { RouterModule } from '@angular/router';
5
+ import { BaseDirective, using, titleCase, SharedModule } from '@thescaffold/ngx-core';
6
+ import * as i1 from '@thescaffold/ngx-ui';
7
+ import { ElementsModule, WebgetsModule } from '@thescaffold/ngx-ui';
8
+ import { BehaviorSubject, takeUntil, map } from 'rxjs';
9
+ import * as i2 from '@angular/common';
10
+
11
+ // P7.1 — ops console section pass (PRD 6.2, TRD 9.2). A standard list
12
+ // view on `x-webget-collection` over `apps/commerce/promotion`, matching the
13
+ // established admin pattern (users, routes, inbox). ops CRUD is covered
14
+ // by `admin:*:apps:commerce:*`.
15
+ class PromotionsComponent extends BaseDirective {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.pluralName = 'promotions';
19
+ this.dataSource = this.apiService.on('apps/commerce/promotion');
20
+ this.columns = [
21
+ { key: 'name', value: 'Name' },
22
+ { key: 'status', value: 'Status', type: 'class', class: 'badge text-bg-light border' },
23
+ { key: 'priority', value: 'Priority' },
24
+ { key: 'usageCount', value: 'Used' },
25
+ ];
26
+ this.page = 1;
27
+ this.perPage = 25;
28
+ this.list$ = new BehaviorSubject(null);
29
+ this.init = () => {
30
+ this.list();
31
+ };
32
+ }
33
+ list(page = this.page, perPage = this.perPage) {
34
+ this.dataSource
35
+ .getMany({ page, perPage })
36
+ .pipe(takeUntil(this.$), map((response) => this.toCollection(response)))
37
+ .subscribe(using((data) => this.list$.next(data)));
38
+ }
39
+ toCollection(response) {
40
+ return {
41
+ title: titleCase(this.pluralName),
42
+ columns: this.columns,
43
+ rows: response?.data,
44
+ hasHeaders: true,
45
+ hasTextFilter: true,
46
+ page: response?.meta?.current_page,
47
+ perPage: response?.meta?.per_page,
48
+ total: response?.meta?.total,
49
+ onPage: (page) => this.list(page),
50
+ };
51
+ }
52
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: PromotionsComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
53
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: PromotionsComponent, isStandalone: false, selector: "x-promotions", usesInheritance: true, ngImport: i0, template: `
54
+ <div class="x-page-head">
55
+ <div class="x-page-title">Promotions</div>
56
+ <div class="x-page-subtitle">Automatic and coupon-gated discount rules evaluated at checkout.</div>
57
+ </div>
58
+
59
+ @if (list$ | async; as list) {
60
+ <x-webget-collection [data]="list"></x-webget-collection>
61
+ } @else {
62
+ <x-element-loader></x-element-loader>
63
+ }
64
+ `, isInline: true, styles: [".x-page-head{margin-bottom:20px}.x-page-title{font-size:24px;font-weight:800;color:var(--color-text)}.x-page-subtitle{font-size:13.5px;color:var(--color-text-muted);margin-top:2px}\n"], dependencies: [{ kind: "component", type: i1.LoaderComponent, selector: "x-element-loader", inputs: ["data"] }, { kind: "component", type: i1.CollectionComponent, selector: "x-webget-collection", inputs: ["data"], outputs: ["search", "filter", "refresh", "select", "page", "show"] }, { kind: "pipe", type: i2.AsyncPipe, name: "async" }] }); }
65
+ }
66
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: PromotionsComponent, decorators: [{
67
+ type: Component,
68
+ args: [{ standalone: false, selector: 'x-promotions', template: `
69
+ <div class="x-page-head">
70
+ <div class="x-page-title">Promotions</div>
71
+ <div class="x-page-subtitle">Automatic and coupon-gated discount rules evaluated at checkout.</div>
72
+ </div>
73
+
74
+ @if (list$ | async; as list) {
75
+ <x-webget-collection [data]="list"></x-webget-collection>
76
+ } @else {
77
+ <x-element-loader></x-element-loader>
78
+ }
79
+ `, styles: [".x-page-head{margin-bottom:20px}.x-page-title{font-size:24px;font-weight:800;color:var(--color-text)}.x-page-subtitle{font-size:13.5px;color:var(--color-text-muted);margin-top:2px}\n"] }]
80
+ }] });
81
+
82
+ class PromotionsModule {
83
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: PromotionsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
84
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: PromotionsModule, declarations: [PromotionsComponent], imports: [SharedModule,
85
+ ElementsModule,
86
+ WebgetsModule, i1$1.RouterModule] }); }
87
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: PromotionsModule, imports: [SharedModule,
88
+ ElementsModule,
89
+ WebgetsModule,
90
+ RouterModule.forChild([{ path: '', component: PromotionsComponent }])] }); }
91
+ }
92
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: PromotionsModule, decorators: [{
93
+ type: NgModule,
94
+ args: [{
95
+ declarations: [PromotionsComponent],
96
+ imports: [
97
+ SharedModule,
98
+ ElementsModule,
99
+ WebgetsModule,
100
+ RouterModule.forChild([{ path: '', component: PromotionsComponent }]),
101
+ ],
102
+ }]
103
+ }] });
104
+
105
+ export { PromotionsModule };
106
+ //# sourceMappingURL=isaiahiroko-tilmart-ngx-apps-admin-promotions.module-DG9uTDqX.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isaiahiroko-tilmart-ngx-apps-admin-promotions.module-DG9uTDqX.mjs","sources":["../../../projects/admin/src/lib/pages/promotions/promotions.component.ts","../../../projects/admin/src/lib/pages/promotions/promotions.module.ts"],"sourcesContent":["import { Component } from '@angular/core';\nimport { BaseDirective, titleCase, using } from '@thescaffold/ngx-core';\nimport { BehaviorSubject, map, takeUntil } from 'rxjs';\n\n// P7.1 — ops console section pass (PRD 6.2, TRD 9.2). A standard list\n// view on `x-webget-collection` over `apps/commerce/promotion`, matching the\n// established admin pattern (users, routes, inbox). ops CRUD is covered\n// by `admin:*:apps:commerce:*`.\n@Component({\n standalone: false,\n selector: 'x-promotions',\n template: `\n <div class=\"x-page-head\">\n <div class=\"x-page-title\">Promotions</div>\n <div class=\"x-page-subtitle\">Automatic and coupon-gated discount rules evaluated at checkout.</div>\n </div>\n\n @if (list$ | async; as list) {\n <x-webget-collection [data]=\"list\"></x-webget-collection>\n } @else {\n <x-element-loader></x-element-loader>\n }\n `,\n styles: [\n `\n .x-page-head {\n margin-bottom: 20px;\n }\n .x-page-title {\n font-size: 24px;\n font-weight: 800;\n color: var(--color-text);\n }\n .x-page-subtitle {\n font-size: 13.5px;\n color: var(--color-text-muted);\n margin-top: 2px;\n }\n `,\n ],\n})\nexport class PromotionsComponent extends BaseDirective {\n pluralName = 'promotions';\n dataSource = this.apiService.on('apps/commerce/promotion');\n\n columns = [\n { key: 'name', value: 'Name' },\n { key: 'status', value: 'Status', type: 'class', class: 'badge text-bg-light border' },\n { key: 'priority', value: 'Priority' },\n { key: 'usageCount', value: 'Used' },\n ];\n\n page = 1;\n perPage = 25;\n\n list$ = new BehaviorSubject<any>(null);\n\n override init = () => {\n this.list();\n };\n\n list(page = this.page, perPage = this.perPage) {\n this.dataSource\n .getMany({ page, perPage })\n .pipe(\n takeUntil(this.$),\n map((response) => this.toCollection(response)),\n )\n .subscribe(using((data) => this.list$.next(data)));\n }\n\n private toCollection(response: any) {\n return {\n title: titleCase(this.pluralName),\n columns: this.columns,\n rows: response?.data,\n hasHeaders: true,\n hasTextFilter: true,\n page: response?.meta?.current_page,\n perPage: response?.meta?.per_page,\n total: response?.meta?.total,\n onPage: (page: number) => this.list(page),\n };\n }\n}\n","import { NgModule } from '@angular/core';\nimport { RouterModule } from '@angular/router';\nimport { SharedModule } from '@thescaffold/ngx-core';\nimport { ElementsModule, WebgetsModule } from '@thescaffold/ngx-ui';\nimport { PromotionsComponent } from './promotions.component';\n\n@NgModule({\n declarations: [PromotionsComponent],\n imports: [\n SharedModule,\n ElementsModule,\n WebgetsModule,\n RouterModule.forChild([{ path: '', component: PromotionsComponent }]),\n ],\n})\nexport class PromotionsModule {}\n"],"names":["i1"],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Component, NgModule } from '@angular/core';
3
+ import * as i1$1 from '@angular/router';
4
+ import { RouterModule } from '@angular/router';
5
+ import { BaseDirective, using, SharedModule } from '@thescaffold/ngx-core';
6
+ import * as i1 from '@thescaffold/ngx-ui';
7
+ import { ElementsModule } from '@thescaffold/ngx-ui';
8
+ import { takeUntil } from 'rxjs';
9
+ import * as i2 from '@angular/common';
10
+
11
+ // P7.3 — TRD §8.10. The ops queue / SLA board: per-queue backlog, oldest
12
+ // waiting item, and an SLA-breach flag, plus the registered cron
13
+ // catalogue so ops can see the scheduled work that keeps the queues
14
+ // moving. Backed by `GET apps/commerce/reports/ops-queues`
15
+ // (`ScheduledWorkflow.opsQueueSnapshot`).
16
+ class QueuesComponent extends BaseDirective {
17
+ constructor() {
18
+ super(...arguments);
19
+ this.source = this.apiService.on('apps/commerce/reports');
20
+ this.loading = true;
21
+ this.queues = [];
22
+ this.crons = [];
23
+ this.init = () => {
24
+ this.load();
25
+ };
26
+ }
27
+ load() {
28
+ this.loading = true;
29
+ this.source
30
+ .getOne('ops-queues')
31
+ .pipe(takeUntil(this.$))
32
+ .subscribe(using((res) => {
33
+ const data = res?.data ?? {};
34
+ this.generatedAt = data.generatedAt;
35
+ this.queues = data.queues ?? [];
36
+ this.crons = data.crons ?? [];
37
+ this.loading = false;
38
+ }));
39
+ }
40
+ get totalBacklog() {
41
+ return this.queues.reduce((sum, q) => sum + q.count, 0);
42
+ }
43
+ get breachCount() {
44
+ return this.queues.filter((q) => q.breached).length;
45
+ }
46
+ ageLabel(hours) {
47
+ if (hours == null)
48
+ return '—';
49
+ if (hours < 1)
50
+ return '<1h';
51
+ if (hours < 48)
52
+ return `${Math.round(hours)}h`;
53
+ return `${Math.round(hours / 24)}d`;
54
+ }
55
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QueuesComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
56
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: QueuesComponent, isStandalone: false, selector: "x-queues", usesInheritance: true, ngImport: i0, template: "<div class=\"x-page-head\">\n <div class=\"x-page-title\">Queues &amp; SLAs</div>\n <div class=\"x-page-subtitle\">\n Live ops backlog with target turnaround, plus the scheduled jobs that\n keep it moving.\n </div>\n</div>\n\n@if (loading) {\n <x-element-loader></x-element-loader>\n} @else {\n <div class=\"x-q-summary\">\n <div class=\"x-q-stat\">\n <div class=\"x-q-stat-value\">{{ totalBacklog }}</div>\n <div class=\"x-q-stat-label\">Items in queue</div>\n </div>\n <div class=\"x-q-stat\" [class.is-bad]=\"breachCount > 0\">\n <div class=\"x-q-stat-value\">{{ breachCount }}</div>\n <div class=\"x-q-stat-label\">SLA breaches</div>\n </div>\n @if (generatedAt) {\n <div class=\"x-q-generated\">as of {{ generatedAt | date: 'short' }}</div>\n }\n </div>\n\n <div class=\"x-q-grid\">\n @for (q of queues; track q.key) {\n <div class=\"x-q-card\" [class.is-breached]=\"q.breached\">\n <div class=\"x-q-card-label\">{{ q.label }}</div>\n <div class=\"x-q-card-count\">{{ q.count }}</div>\n <div class=\"x-q-card-meta\">\n <span>oldest {{ ageLabel(q.oldestAgeHours) }}</span>\n @if (q.slaHours != null) {\n <span class=\"x-q-sla\" [class.is-breached]=\"q.breached\">\n SLA {{ q.slaHours }}h\n </span>\n }\n </div>\n </div>\n }\n </div>\n\n <h2 class=\"x-q-crons-title\">Scheduled jobs</h2>\n <table class=\"x-q-crons\">\n <thead>\n <tr><th>Job</th><th>Schedule</th><th>Purpose</th></tr>\n </thead>\n <tbody>\n @for (c of crons; track c.name) {\n <tr>\n <td class=\"x-q-cron-name\">{{ c.name }}</td>\n <td class=\"x-q-cron-pattern\">{{ c.pattern }}</td>\n <td>{{ c.purpose }}</td>\n </tr>\n }\n </tbody>\n </table>\n}\n\n<style>\n .x-page-head { margin-bottom: 20px; }\n .x-page-title { font-size: 24px; font-weight: 800; color: var(--color-text); }\n .x-page-subtitle { font-size: 13.5px; color: var(--color-text-muted); margin-top: 2px; max-width: 620px; }\n\n .x-q-summary { display: flex; align-items: center; gap: 20px; margin-bottom: 20px; }\n .x-q-stat { border: 1px solid var(--color-border); border-radius: 10px; padding: 10px 16px; min-width: 110px; }\n .x-q-stat.is-bad { border-color: var(--color-danger, #c0392b); }\n .x-q-stat-value { font-size: 22px; font-weight: 800; color: var(--color-text); }\n .x-q-stat-label { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--color-text-muted); }\n .x-q-generated { font-size: 12px; color: var(--color-text-muted); }\n\n .x-q-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-bottom: 32px; }\n .x-q-card { border: 1px solid var(--color-border); border-radius: 10px; padding: 12px 14px; }\n .x-q-card.is-breached { border-color: var(--color-danger, #c0392b); background: color-mix(in srgb, var(--color-danger, #c0392b) 6%, transparent); }\n .x-q-card-label { font-size: 12.5px; font-weight: 600; color: var(--color-text-muted); }\n .x-q-card-count { font-size: 26px; font-weight: 800; color: var(--color-text); margin: 4px 0 6px; }\n .x-q-card-meta { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--color-text-muted); }\n .x-q-sla.is-breached { color: var(--color-danger, #c0392b); font-weight: 700; }\n\n .x-q-crons-title { font-size: 15px; font-weight: 700; color: var(--color-text); margin: 0 0 10px; }\n .x-q-crons { width: 100%; border-collapse: collapse; font-size: 13px; }\n .x-q-crons th, .x-q-crons td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--color-border); }\n .x-q-crons th { color: var(--color-text-muted); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.03em; }\n .x-q-cron-name, .x-q-cron-pattern { font-family: var(--font-mono, monospace); font-size: 12px; }\n</style>\n", styles: [".x-page-head{margin-bottom:20px}.x-page-title{font-size:24px;font-weight:800;color:var(--color-text)}.x-page-subtitle{font-size:13.5px;color:var(--color-text-muted);margin-top:2px;max-width:620px}.x-q-summary{display:flex;align-items:center;gap:20px;margin-bottom:20px}.x-q-stat{border:1px solid var(--color-border);border-radius:10px;padding:10px 16px;min-width:110px}.x-q-stat.is-bad{border-color:var(--color-danger, #c0392b)}.x-q-stat-value{font-size:22px;font-weight:800;color:var(--color-text)}.x-q-stat-label{font-size:11.5px;text-transform:uppercase;letter-spacing:.03em;color:var(--color-text-muted)}.x-q-generated{font-size:12px;color:var(--color-text-muted)}.x-q-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;margin-bottom:32px}.x-q-card{border:1px solid var(--color-border);border-radius:10px;padding:12px 14px}.x-q-card.is-breached{border-color:var(--color-danger, #c0392b);background:color-mix(in srgb,var(--color-danger, #c0392b) 6%,transparent)}.x-q-card-label{font-size:12.5px;font-weight:600;color:var(--color-text-muted)}.x-q-card-count{font-size:26px;font-weight:800;color:var(--color-text);margin:4px 0 6px}.x-q-card-meta{display:flex;justify-content:space-between;font-size:11.5px;color:var(--color-text-muted)}.x-q-sla.is-breached{color:var(--color-danger, #c0392b);font-weight:700}.x-q-crons-title{font-size:15px;font-weight:700;color:var(--color-text);margin:0 0 10px}.x-q-crons{width:100%;border-collapse:collapse;font-size:13px}.x-q-crons th,.x-q-crons td{text-align:left;padding:7px 10px;border-bottom:1px solid var(--color-border)}.x-q-crons th{color:var(--color-text-muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.03em}.x-q-cron-name,.x-q-cron-pattern{font-family:var(--font-mono, monospace);font-size:12px}\n"], dependencies: [{ kind: "component", type: i1.LoaderComponent, selector: "x-element-loader", inputs: ["data"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }] }); }
57
+ }
58
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QueuesComponent, decorators: [{
59
+ type: Component,
60
+ args: [{ standalone: false, selector: 'x-queues', template: "<div class=\"x-page-head\">\n <div class=\"x-page-title\">Queues &amp; SLAs</div>\n <div class=\"x-page-subtitle\">\n Live ops backlog with target turnaround, plus the scheduled jobs that\n keep it moving.\n </div>\n</div>\n\n@if (loading) {\n <x-element-loader></x-element-loader>\n} @else {\n <div class=\"x-q-summary\">\n <div class=\"x-q-stat\">\n <div class=\"x-q-stat-value\">{{ totalBacklog }}</div>\n <div class=\"x-q-stat-label\">Items in queue</div>\n </div>\n <div class=\"x-q-stat\" [class.is-bad]=\"breachCount > 0\">\n <div class=\"x-q-stat-value\">{{ breachCount }}</div>\n <div class=\"x-q-stat-label\">SLA breaches</div>\n </div>\n @if (generatedAt) {\n <div class=\"x-q-generated\">as of {{ generatedAt | date: 'short' }}</div>\n }\n </div>\n\n <div class=\"x-q-grid\">\n @for (q of queues; track q.key) {\n <div class=\"x-q-card\" [class.is-breached]=\"q.breached\">\n <div class=\"x-q-card-label\">{{ q.label }}</div>\n <div class=\"x-q-card-count\">{{ q.count }}</div>\n <div class=\"x-q-card-meta\">\n <span>oldest {{ ageLabel(q.oldestAgeHours) }}</span>\n @if (q.slaHours != null) {\n <span class=\"x-q-sla\" [class.is-breached]=\"q.breached\">\n SLA {{ q.slaHours }}h\n </span>\n }\n </div>\n </div>\n }\n </div>\n\n <h2 class=\"x-q-crons-title\">Scheduled jobs</h2>\n <table class=\"x-q-crons\">\n <thead>\n <tr><th>Job</th><th>Schedule</th><th>Purpose</th></tr>\n </thead>\n <tbody>\n @for (c of crons; track c.name) {\n <tr>\n <td class=\"x-q-cron-name\">{{ c.name }}</td>\n <td class=\"x-q-cron-pattern\">{{ c.pattern }}</td>\n <td>{{ c.purpose }}</td>\n </tr>\n }\n </tbody>\n </table>\n}\n\n<style>\n .x-page-head { margin-bottom: 20px; }\n .x-page-title { font-size: 24px; font-weight: 800; color: var(--color-text); }\n .x-page-subtitle { font-size: 13.5px; color: var(--color-text-muted); margin-top: 2px; max-width: 620px; }\n\n .x-q-summary { display: flex; align-items: center; gap: 20px; margin-bottom: 20px; }\n .x-q-stat { border: 1px solid var(--color-border); border-radius: 10px; padding: 10px 16px; min-width: 110px; }\n .x-q-stat.is-bad { border-color: var(--color-danger, #c0392b); }\n .x-q-stat-value { font-size: 22px; font-weight: 800; color: var(--color-text); }\n .x-q-stat-label { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--color-text-muted); }\n .x-q-generated { font-size: 12px; color: var(--color-text-muted); }\n\n .x-q-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-bottom: 32px; }\n .x-q-card { border: 1px solid var(--color-border); border-radius: 10px; padding: 12px 14px; }\n .x-q-card.is-breached { border-color: var(--color-danger, #c0392b); background: color-mix(in srgb, var(--color-danger, #c0392b) 6%, transparent); }\n .x-q-card-label { font-size: 12.5px; font-weight: 600; color: var(--color-text-muted); }\n .x-q-card-count { font-size: 26px; font-weight: 800; color: var(--color-text); margin: 4px 0 6px; }\n .x-q-card-meta { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--color-text-muted); }\n .x-q-sla.is-breached { color: var(--color-danger, #c0392b); font-weight: 700; }\n\n .x-q-crons-title { font-size: 15px; font-weight: 700; color: var(--color-text); margin: 0 0 10px; }\n .x-q-crons { width: 100%; border-collapse: collapse; font-size: 13px; }\n .x-q-crons th, .x-q-crons td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--color-border); }\n .x-q-crons th { color: var(--color-text-muted); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.03em; }\n .x-q-cron-name, .x-q-cron-pattern { font-family: var(--font-mono, monospace); font-size: 12px; }\n</style>\n" }]
61
+ }] });
62
+
63
+ class QueuesModule {
64
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QueuesModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
65
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.16", ngImport: i0, type: QueuesModule, declarations: [QueuesComponent], imports: [SharedModule,
66
+ ElementsModule, i1$1.RouterModule] }); }
67
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QueuesModule, imports: [SharedModule,
68
+ ElementsModule,
69
+ RouterModule.forChild([{ path: '', component: QueuesComponent }])] }); }
70
+ }
71
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QueuesModule, decorators: [{
72
+ type: NgModule,
73
+ args: [{
74
+ declarations: [QueuesComponent],
75
+ imports: [
76
+ SharedModule,
77
+ ElementsModule,
78
+ RouterModule.forChild([{ path: '', component: QueuesComponent }]),
79
+ ],
80
+ }]
81
+ }] });
82
+
83
+ export { QueuesModule };
84
+ //# sourceMappingURL=isaiahiroko-tilmart-ngx-apps-admin-queues.module-K-zYYVFy.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isaiahiroko-tilmart-ngx-apps-admin-queues.module-K-zYYVFy.mjs","sources":["../../../projects/admin/src/lib/pages/queues/queues.component.ts","../../../projects/admin/src/lib/pages/queues/queues.component.html","../../../projects/admin/src/lib/pages/queues/queues.module.ts"],"sourcesContent":["import { Component } from '@angular/core';\nimport { BaseDirective, using } from '@thescaffold/ngx-core';\nimport { takeUntil } from 'rxjs';\n\ninterface OpsQueue {\n key: string;\n label: string;\n count: number;\n oldestAgeHours: number | null;\n slaHours: number | null;\n breached: boolean;\n}\n\ninterface CronDef {\n name: string;\n pattern: string;\n purpose: string;\n}\n\n// P7.3 — TRD §8.10. The ops queue / SLA board: per-queue backlog, oldest\n// waiting item, and an SLA-breach flag, plus the registered cron\n// catalogue so ops can see the scheduled work that keeps the queues\n// moving. Backed by `GET apps/commerce/reports/ops-queues`\n// (`ScheduledWorkflow.opsQueueSnapshot`).\n@Component({\n standalone: false,\n selector: 'x-queues',\n templateUrl: './queues.component.html',\n})\nexport class QueuesComponent extends BaseDirective {\n private readonly source = this.apiService.on('apps/commerce/reports');\n\n loading = true;\n generatedAt?: string;\n queues: OpsQueue[] = [];\n crons: CronDef[] = [];\n\n override init = () => {\n this.load();\n };\n\n load(): void {\n this.loading = true;\n this.source\n .getOne('ops-queues')\n .pipe(takeUntil(this.$))\n .subscribe(\n using((res: any) => {\n const data = res?.data ?? {};\n this.generatedAt = data.generatedAt;\n this.queues = data.queues ?? [];\n this.crons = data.crons ?? [];\n this.loading = false;\n }),\n );\n }\n\n get totalBacklog(): number {\n return this.queues.reduce((sum, q) => sum + q.count, 0);\n }\n\n get breachCount(): number {\n return this.queues.filter((q) => q.breached).length;\n }\n\n ageLabel(hours: number | null): string {\n if (hours == null) return '—';\n if (hours < 1) return '<1h';\n if (hours < 48) return `${Math.round(hours)}h`;\n return `${Math.round(hours / 24)}d`;\n }\n}\n","<div class=\"x-page-head\">\n <div class=\"x-page-title\">Queues &amp; SLAs</div>\n <div class=\"x-page-subtitle\">\n Live ops backlog with target turnaround, plus the scheduled jobs that\n keep it moving.\n </div>\n</div>\n\n@if (loading) {\n <x-element-loader></x-element-loader>\n} @else {\n <div class=\"x-q-summary\">\n <div class=\"x-q-stat\">\n <div class=\"x-q-stat-value\">{{ totalBacklog }}</div>\n <div class=\"x-q-stat-label\">Items in queue</div>\n </div>\n <div class=\"x-q-stat\" [class.is-bad]=\"breachCount > 0\">\n <div class=\"x-q-stat-value\">{{ breachCount }}</div>\n <div class=\"x-q-stat-label\">SLA breaches</div>\n </div>\n @if (generatedAt) {\n <div class=\"x-q-generated\">as of {{ generatedAt | date: 'short' }}</div>\n }\n </div>\n\n <div class=\"x-q-grid\">\n @for (q of queues; track q.key) {\n <div class=\"x-q-card\" [class.is-breached]=\"q.breached\">\n <div class=\"x-q-card-label\">{{ q.label }}</div>\n <div class=\"x-q-card-count\">{{ q.count }}</div>\n <div class=\"x-q-card-meta\">\n <span>oldest {{ ageLabel(q.oldestAgeHours) }}</span>\n @if (q.slaHours != null) {\n <span class=\"x-q-sla\" [class.is-breached]=\"q.breached\">\n SLA {{ q.slaHours }}h\n </span>\n }\n </div>\n </div>\n }\n </div>\n\n <h2 class=\"x-q-crons-title\">Scheduled jobs</h2>\n <table class=\"x-q-crons\">\n <thead>\n <tr><th>Job</th><th>Schedule</th><th>Purpose</th></tr>\n </thead>\n <tbody>\n @for (c of crons; track c.name) {\n <tr>\n <td class=\"x-q-cron-name\">{{ c.name }}</td>\n <td class=\"x-q-cron-pattern\">{{ c.pattern }}</td>\n <td>{{ c.purpose }}</td>\n </tr>\n }\n </tbody>\n </table>\n}\n\n<style>\n .x-page-head { margin-bottom: 20px; }\n .x-page-title { font-size: 24px; font-weight: 800; color: var(--color-text); }\n .x-page-subtitle { font-size: 13.5px; color: var(--color-text-muted); margin-top: 2px; max-width: 620px; }\n\n .x-q-summary { display: flex; align-items: center; gap: 20px; margin-bottom: 20px; }\n .x-q-stat { border: 1px solid var(--color-border); border-radius: 10px; padding: 10px 16px; min-width: 110px; }\n .x-q-stat.is-bad { border-color: var(--color-danger, #c0392b); }\n .x-q-stat-value { font-size: 22px; font-weight: 800; color: var(--color-text); }\n .x-q-stat-label { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--color-text-muted); }\n .x-q-generated { font-size: 12px; color: var(--color-text-muted); }\n\n .x-q-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-bottom: 32px; }\n .x-q-card { border: 1px solid var(--color-border); border-radius: 10px; padding: 12px 14px; }\n .x-q-card.is-breached { border-color: var(--color-danger, #c0392b); background: color-mix(in srgb, var(--color-danger, #c0392b) 6%, transparent); }\n .x-q-card-label { font-size: 12.5px; font-weight: 600; color: var(--color-text-muted); }\n .x-q-card-count { font-size: 26px; font-weight: 800; color: var(--color-text); margin: 4px 0 6px; }\n .x-q-card-meta { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--color-text-muted); }\n .x-q-sla.is-breached { color: var(--color-danger, #c0392b); font-weight: 700; }\n\n .x-q-crons-title { font-size: 15px; font-weight: 700; color: var(--color-text); margin: 0 0 10px; }\n .x-q-crons { width: 100%; border-collapse: collapse; font-size: 13px; }\n .x-q-crons th, .x-q-crons td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--color-border); }\n .x-q-crons th { color: var(--color-text-muted); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.03em; }\n .x-q-cron-name, .x-q-cron-pattern { font-family: var(--font-mono, monospace); font-size: 12px; }\n</style>\n","import { NgModule } from '@angular/core';\nimport { RouterModule } from '@angular/router';\nimport { SharedModule } from '@thescaffold/ngx-core';\nimport { ElementsModule } from '@thescaffold/ngx-ui';\nimport { QueuesComponent } from './queues.component';\n\n@NgModule({\n declarations: [QueuesComponent],\n imports: [\n SharedModule,\n ElementsModule,\n RouterModule.forChild([{ path: '', component: QueuesComponent }]),\n ],\n})\nexport class QueuesModule {}\n"],"names":["i1"],"mappings":"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"}