tin-spa 20.12.1 → 20.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/tin-spa.mjs +548 -79
- package/fesm2022/tin-spa.mjs.map +1 -1
- package/index.d.ts +65 -17
- package/package.json +1 -1
package/fesm2022/tin-spa.mjs
CHANGED
|
@@ -3105,7 +3105,7 @@ class DataServiceLib {
|
|
|
3105
3105
|
this.capServiceItems = new CapItem; // Added: Capability for service items
|
|
3106
3106
|
this.capBundleProducts = new CapItem; // Added: Capability for bundle products
|
|
3107
3107
|
this.capInventoryItems = new CapItem;
|
|
3108
|
-
this.
|
|
3108
|
+
this.capSupplierPurchaseOrders = new CapItem;
|
|
3109
3109
|
this.capInventoryReceipts = new CapItem;
|
|
3110
3110
|
this.capSalesOrders = new CapItem;
|
|
3111
3111
|
this.capSales = new CapItem;
|
|
@@ -3510,8 +3510,13 @@ class DataServiceLib {
|
|
|
3510
3510
|
{ name: 'email', type: 'text' },
|
|
3511
3511
|
{ name: 'address', type: 'text-area', rows: 2, span: true },
|
|
3512
3512
|
{ name: 'customerTenantID', alias: 'Customer Tenant', type: 'text-single', loadAction: { url: 'tenants/list/x' }, strict: true, nullable: true }, // Changed: no longer required — CustomerTenantID is nullable on the model (IsLinked is the optional B2B link), and a required server-loaded lookup made offline create impossible
|
|
3513
|
-
|
|
3514
|
-
|
|
3513
|
+
// Added: statutory and trading terms — the details that are agreed once with the customer and
|
|
3514
|
+
// then govern how every document to them must be raised. Grouped away from contact details
|
|
3515
|
+
// because nobody edits these day to day, but getting them wrong invalidates an invoice.
|
|
3516
|
+
{ name: 'tradingTerms', type: 'section', alias: 'Statutory & Trading Terms' },
|
|
3517
|
+
{ name: 'vatNumber', type: 'text', alias: 'VAT Number', hint: 'ZIMRA VAT registration — required on a tax invoice' }, // Added: Phase 2 compliance — customer VAT registration number
|
|
3518
|
+
{ name: 'tin', type: 'text', alias: 'TIN', hint: 'Taxpayer Identification Number' }, // Added: Phase 2 compliance — taxpayer identification number
|
|
3519
|
+
{ name: 'requirePONumber', type: 'checkbox', alias: 'Requires a PO number', hint: 'This customer rejects an invoice that does not quote their own order number' }, // Added: moved off the tenant-wide AppConfiguration — PO discipline is a term of the trading relationship
|
|
3515
3520
|
],
|
|
3516
3521
|
loadAction: { url: 'customers/id' },
|
|
3517
3522
|
heroField: 'customerID',
|
|
@@ -3564,6 +3569,63 @@ class DataServiceLib {
|
|
|
3564
3569
|
offline: { enabled: true, queue: { create: true, edit: true, delete: true } }, // Added: reads served from cache offline, writes queued and drained on reconnect; RowVersion catches conflicting edits
|
|
3565
3570
|
entityName: 'Customer' // Changed: Match backend typeof(Customer).Name for SignalR broadcasts
|
|
3566
3571
|
};
|
|
3572
|
+
//--------------------------Customer Purchase Orders-------------------------
|
|
3573
|
+
// Orders the CUSTOMER issues to us, against which we bill. Distinct from the supplier purchase
|
|
3574
|
+
// order (Purchasing), which is one WE issue when buying — hence the fully-qualified names on both
|
|
3575
|
+
// sides, so "purchase order" is never ambiguous in code or on screen.
|
|
3576
|
+
// Entirely optional: a cash-based customer is invoiced with no order at all. Recording one simply
|
|
3577
|
+
// makes "how much of this order is still unbilled" answerable, and lets over-billing be spotted.
|
|
3578
|
+
this.customerPurchaseOrderFormConfig = {
|
|
3579
|
+
security: { allow: [this.capCustomers] },
|
|
3580
|
+
includeAudit: true,
|
|
3581
|
+
title: 'Customer Purchase Order',
|
|
3582
|
+
fields: [
|
|
3583
|
+
{ name: 'customerID', alias: 'Customer', type: 'text-single', loadAction: { url: 'customers/list/x' }, required: true, strict: true },
|
|
3584
|
+
{ name: 'poNumber', type: 'text', alias: 'Order Number', required: true, hint: 'Exactly as the customer wrote it — this is the field they reconcile on' },
|
|
3585
|
+
{ name: 'poDate', type: 'date', alias: 'Order Date', required: true },
|
|
3586
|
+
{ name: 'amount', type: 'number', alias: 'Order Value (excl tax)', hint: 'Leave 0 if the order arrived without a total — billing is never blocked, it just cannot be checked against a value' },
|
|
3587
|
+
{ name: 'expiryDate', type: 'date', alias: 'Expires', nullable: true },
|
|
3588
|
+
{ name: 'status', type: 'select', options: [{ id: 0, name: 'Open' }, { id: 1, name: 'Closed' }, { id: 2, name: 'Cancelled' }] },
|
|
3589
|
+
{ name: 'reference', type: 'text', alias: 'Their Reference', hint: 'Requisition, contract or job-card number. Two orders can share one — it is not unique' },
|
|
3590
|
+
{ name: 'notes', type: 'text-area', rows: 2, span: true },
|
|
3591
|
+
],
|
|
3592
|
+
loadAction: { url: 'customerpurchaseorders/id' },
|
|
3593
|
+
heroField: 'customerPurchaseOrderID',
|
|
3594
|
+
reset: true,
|
|
3595
|
+
};
|
|
3596
|
+
this.customerPurchaseOrdersTableConfig = {
|
|
3597
|
+
showFilter: true,
|
|
3598
|
+
minColumns: ['poNumber', 'customerName', 'amountRemaining'],
|
|
3599
|
+
flatButtons: true,
|
|
3600
|
+
maxButtonsCount: 3,
|
|
3601
|
+
collapseButtons: true,
|
|
3602
|
+
columns: [
|
|
3603
|
+
{ name: 'poNumber', type: 'text', alias: 'Order' },
|
|
3604
|
+
{ name: 'customerName', type: 'text', alias: 'Customer' },
|
|
3605
|
+
{ name: 'poDate', type: 'date', alias: 'Dated' },
|
|
3606
|
+
{ name: 'amount', type: 'number', alias: 'Value' },
|
|
3607
|
+
{ name: 'amountBilled', type: 'number', alias: 'Billed' },
|
|
3608
|
+
{ name: 'lineCount', type: 'number', alias: 'Lines' },
|
|
3609
|
+
{
|
|
3610
|
+
name: 'amountRemaining', type: 'chip', alias: 'Left to bill',
|
|
3611
|
+
colors: [
|
|
3612
|
+
{ name: '#EF9A9A', condition: x => x.isOverBilled }, // billed past what the customer authorised
|
|
3613
|
+
{ name: '#A5D6A7', condition: x => !x.isOverBilled && x.amountRemaining > 0 },
|
|
3614
|
+
{ name: '#E0E0E0', condition: x => !x.isOverBilled && x.amountRemaining <= 0 } // fully consumed, nothing left to bill
|
|
3615
|
+
]
|
|
3616
|
+
},
|
|
3617
|
+
{ name: 'status', type: 'text' },
|
|
3618
|
+
],
|
|
3619
|
+
buttons: [
|
|
3620
|
+
{ name: 'create', display: 'Record Order', dialog: true, action: { url: 'customerpurchaseorders?action=create', method: 'post' } },
|
|
3621
|
+
{ name: 'view', dialog: true },
|
|
3622
|
+
{ name: 'edit', dialog: true, action: { url: 'customerpurchaseorders?action=edit', method: 'post' } },
|
|
3623
|
+
{ name: 'delete', dialog: true, action: { url: 'customerpurchaseorders?action=delete', method: 'post' } },
|
|
3624
|
+
],
|
|
3625
|
+
loadAction: { url: 'customerpurchaseorders/all/x' },
|
|
3626
|
+
formConfig: this.customerPurchaseOrderFormConfig,
|
|
3627
|
+
entityName: 'CustomerPurchaseOrder'
|
|
3628
|
+
};
|
|
3567
3629
|
//--------------------------Suppliers-------------------------
|
|
3568
3630
|
this.supplierFormConfig = {
|
|
3569
3631
|
security: { allow: [this.capSuppliers] },
|
|
@@ -4204,7 +4266,7 @@ class DataServiceLib {
|
|
|
4204
4266
|
this.capPurchasingModule.name = "cap62";
|
|
4205
4267
|
this.capPurchasingModule.display = "Purchasing";
|
|
4206
4268
|
this.capPurchasingModule.icon = "shopping_cart";
|
|
4207
|
-
this.capPurchasingModule.capSubItems = [this.capPurchasingDashboard, this.capInventoryReceipts, this.
|
|
4269
|
+
this.capPurchasingModule.capSubItems = [this.capPurchasingDashboard, this.capInventoryReceipts, this.capSupplierPurchaseOrders, this.capSupplierAging]; // Changed: Added Dashboard to Purchasing submenu
|
|
4208
4270
|
this.capPurchasingDashboard.name = "cap62"; // Changed: Reuses module cap number for dashboard visibility
|
|
4209
4271
|
this.capPurchasingDashboard.display = "Dashboard";
|
|
4210
4272
|
this.capPurchasingDashboard.link = "home/purchasing/dashboard";
|
|
@@ -4246,10 +4308,10 @@ class DataServiceLib {
|
|
|
4246
4308
|
this.capInventoryItems.display = "Inventory Items";
|
|
4247
4309
|
this.capInventoryItems.link = "home/inventory/items";
|
|
4248
4310
|
this.capInventoryItems.icon = "inventory_2";
|
|
4249
|
-
this.
|
|
4250
|
-
this.
|
|
4251
|
-
this.
|
|
4252
|
-
this.
|
|
4311
|
+
this.capSupplierPurchaseOrders.name = "cap47";
|
|
4312
|
+
this.capSupplierPurchaseOrders.display = "Purchase Orders";
|
|
4313
|
+
this.capSupplierPurchaseOrders.link = "home/purchasing/orders";
|
|
4314
|
+
this.capSupplierPurchaseOrders.icon = "shopping_bag";
|
|
4253
4315
|
this.capInventoryReceipts.name = "cap38";
|
|
4254
4316
|
this.capInventoryReceipts.display = "Purchases"; // Changed: "Receipts" → "Purchases"
|
|
4255
4317
|
this.capInventoryReceipts.link = "home/purchasing/purchases"; // Changed: inventory-receipts → purchases
|
|
@@ -6556,12 +6618,16 @@ class AccountingService {
|
|
|
6556
6618
|
heroField: 'creditNoteID'
|
|
6557
6619
|
};
|
|
6558
6620
|
// Credit note action buttons
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6621
|
+
// Changed: issuing now also settles the linked invoice in the same step, so the message says so —
|
|
6622
|
+
// it used to leave the ledger crediting AR while the invoice still showed the full balance.
|
|
6623
|
+
this.creditNoteSubmitButton = { name: 'submit', inDialog: true, display: 'Issue', icon: { name: 'send' },
|
|
6624
|
+
action: { url: 'creditnotes?action=submit', method: 'post', successMessage: 'Credit note issued' },
|
|
6625
|
+
confirm: { message: 'Issue this credit note? It reverses revenue and VAT in the ledger and credits the linked invoice.' },
|
|
6562
6626
|
visible: x => x.status == CreditNoteStatus.Draft,
|
|
6563
6627
|
disabled: x => x.totalAmount == 0
|
|
6564
6628
|
};
|
|
6629
|
+
// Changed: only needed now for a credit raised against an invoice that had no balance left at the
|
|
6630
|
+
// time (a return after payment) and is later netted off. The normal path applies on issue.
|
|
6565
6631
|
this.creditNoteApplyButton = { name: 'apply', inDialog: true, display: 'Apply to Invoice', icon: { name: 'link', color: 'blue' },
|
|
6566
6632
|
action: { url: 'creditnotes?action=apply', method: 'post', successMessage: 'Credit applied to invoice' },
|
|
6567
6633
|
confirm: { message: 'Apply this credit against the linked invoice balance?' },
|
|
@@ -8367,6 +8433,10 @@ class TabService {
|
|
|
8367
8433
|
return value !== undefined && value !== null ? String(value) : match;
|
|
8368
8434
|
});
|
|
8369
8435
|
}
|
|
8436
|
+
// Added: resolved count URL for a tab — public so hosts can tell when the parent record finally supplies the id
|
|
8437
|
+
resolveCountUrl(config, parentData) {
|
|
8438
|
+
return config?.countAction?.url ? this.transformCountUrl(config.countAction.url, parentData) : '';
|
|
8439
|
+
}
|
|
8370
8440
|
// Initialize tab counts and reload subjects for all tabs
|
|
8371
8441
|
initializeTabs(tableConfigs, parentData) {
|
|
8372
8442
|
const tabCounts = {};
|
|
@@ -8384,6 +8454,8 @@ class TabService {
|
|
|
8384
8454
|
) {
|
|
8385
8455
|
// Changed: Transform URL by replacing {propertyName} placeholders with parentData values
|
|
8386
8456
|
const transformedUrl = this.transformCountUrl(countAction.url, parentData);
|
|
8457
|
+
if (transformedUrl?.includes('{'))
|
|
8458
|
+
return; // Added: parent record not loaded yet — firing with the raw {id} placeholder makes the API drop the parent filter and answer with a GLOBAL count (the stale badge bug)
|
|
8387
8459
|
const transformedAction = { ...countAction, url: transformedUrl };
|
|
8388
8460
|
this.dataService.CallApi(transformedAction).subscribe((apiResponse) => {
|
|
8389
8461
|
if (apiResponse.success) {
|
|
@@ -8738,6 +8810,81 @@ class AgentService {
|
|
|
8738
8810
|
error: () => this.typing.next(false)
|
|
8739
8811
|
});
|
|
8740
8812
|
}
|
|
8813
|
+
// Transcribe a recording WITHOUT invoking the agent — backs the composer's Stop button, so the user can
|
|
8814
|
+
// review and edit what was heard before deciding to send.
|
|
8815
|
+
transcribeOnly(audio, fileName) {
|
|
8816
|
+
const form = new FormData();
|
|
8817
|
+
form.append('audio', audio, fileName);
|
|
8818
|
+
return this.dataService.CallApi({ url: 'agent/transcribe', method: 'post', isFormData: true }, form);
|
|
8819
|
+
}
|
|
8820
|
+
// Send a composed message — any combination of a staged photo, a voice clip and typed text, in one go.
|
|
8821
|
+
// A placeholder user bubble goes up immediately and is swapped for the derived text when the backend
|
|
8822
|
+
// replies, which also keeps it above the assistant's answer (that arrives over SignalR mid-request).
|
|
8823
|
+
sendComposed(photo, photoName, audio, audioName, caption = '') {
|
|
8824
|
+
const hasPhoto = !!(photo && photo.size);
|
|
8825
|
+
const hasAudio = !!(audio && audio.size);
|
|
8826
|
+
if (!hasPhoto && !hasAudio && !caption.trim())
|
|
8827
|
+
return;
|
|
8828
|
+
const placeholder = {
|
|
8829
|
+
role: 'user',
|
|
8830
|
+
content: caption.trim() || (hasAudio ? 'Voice note…' : 'Photo…'),
|
|
8831
|
+
createdDate: new Date(),
|
|
8832
|
+
pending: true
|
|
8833
|
+
};
|
|
8834
|
+
this.messages.next([...this.messages.value, placeholder]);
|
|
8835
|
+
this.typing.next(true);
|
|
8836
|
+
const form = new FormData();
|
|
8837
|
+
if (hasPhoto)
|
|
8838
|
+
form.append('photo', photo, photoName);
|
|
8839
|
+
if (hasAudio)
|
|
8840
|
+
form.append('audio', audio, audioName);
|
|
8841
|
+
form.append('text', caption.trim());
|
|
8842
|
+
form.append('appName', this.appName);
|
|
8843
|
+
this.dataService.CallApi({ url: 'agent/media', method: 'post', isFormData: true }, form).subscribe({
|
|
8844
|
+
next: (res) => {
|
|
8845
|
+
this.typing.next(false);
|
|
8846
|
+
if (res?.success && res.data?.inputText) {
|
|
8847
|
+
this.replaceMessage(placeholder, { ...placeholder, content: res.data.inputText, pending: false });
|
|
8848
|
+
return;
|
|
8849
|
+
}
|
|
8850
|
+
// Nothing usable came back — drop the placeholder and say why
|
|
8851
|
+
this.removeMessage(placeholder);
|
|
8852
|
+
this.messages.next([...this.messages.value, {
|
|
8853
|
+
role: 'assistant',
|
|
8854
|
+
content: res?.message || 'Sorry, I could not process that. Please try again.',
|
|
8855
|
+
createdDate: new Date()
|
|
8856
|
+
}]);
|
|
8857
|
+
},
|
|
8858
|
+
error: () => {
|
|
8859
|
+
this.typing.next(false);
|
|
8860
|
+
this.removeMessage(placeholder);
|
|
8861
|
+
this.messages.next([...this.messages.value, {
|
|
8862
|
+
role: 'assistant',
|
|
8863
|
+
content: 'Sorry, that could not be sent. Please check your connection and try again.',
|
|
8864
|
+
createdDate: new Date()
|
|
8865
|
+
}]);
|
|
8866
|
+
}
|
|
8867
|
+
});
|
|
8868
|
+
}
|
|
8869
|
+
// Changed: kept so existing callers of the single-attachment API keep working
|
|
8870
|
+
sendMedia(file, fileName, kind, caption = '') {
|
|
8871
|
+
if (kind === 'voice')
|
|
8872
|
+
this.sendComposed(null, '', file, fileName, caption);
|
|
8873
|
+
else
|
|
8874
|
+
this.sendComposed(file, fileName, null, '', caption);
|
|
8875
|
+
}
|
|
8876
|
+
// Swap a message in place so its position in the thread is preserved
|
|
8877
|
+
replaceMessage(target, updated) {
|
|
8878
|
+
const msgs = this.messages.value.slice();
|
|
8879
|
+
const index = msgs.indexOf(target);
|
|
8880
|
+
if (index < 0)
|
|
8881
|
+
return;
|
|
8882
|
+
msgs[index] = updated;
|
|
8883
|
+
this.messages.next(msgs);
|
|
8884
|
+
}
|
|
8885
|
+
removeMessage(target) {
|
|
8886
|
+
this.messages.next(this.messages.value.filter(m => m !== target));
|
|
8887
|
+
}
|
|
8741
8888
|
// Start a new conversation — only when user explicitly clicks "New conversation"
|
|
8742
8889
|
newConversation() {
|
|
8743
8890
|
this.messages.next([]);
|
|
@@ -8979,7 +9126,30 @@ class LastRouteService {
|
|
|
8979
9126
|
return null;
|
|
8980
9127
|
const map = this.storage.getPersistent(Constants.LAST_ROUTE) || {};
|
|
8981
9128
|
const route = map[user];
|
|
8982
|
-
|
|
9129
|
+
if (!this.isResumable(route))
|
|
9130
|
+
return null;
|
|
9131
|
+
// Changed: a page the role can no longer open (capability revoked, menu item retired) must not be resumed —
|
|
9132
|
+
// drop it so the next visit goes straight to the default page instead of re-testing a dead entry
|
|
9133
|
+
if (!(await this.allowed(route))) {
|
|
9134
|
+
await this.clear();
|
|
9135
|
+
return null;
|
|
9136
|
+
}
|
|
9137
|
+
return route;
|
|
9138
|
+
}
|
|
9139
|
+
// Added: single navigation entry point for callers resuming a session. Falls back to the app's default page
|
|
9140
|
+
// when the saved route no longer exists — without this a removed route falls through the app's wildcard
|
|
9141
|
+
// route (which redirects back to the landing page) and the user bounces instead of landing anywhere.
|
|
9142
|
+
async navigateTo(target, fallback = 'home') {
|
|
9143
|
+
if (!target || target === fallback) {
|
|
9144
|
+
this.router.navigate([fallback]);
|
|
9145
|
+
return;
|
|
9146
|
+
}
|
|
9147
|
+
const ok = await this.router.navigate([target]).catch(() => false);
|
|
9148
|
+
const landed = this.strip(this.router.url);
|
|
9149
|
+
if (ok && (landed === target || landed.startsWith(target + '/')))
|
|
9150
|
+
return;
|
|
9151
|
+
await this.clear(); // stale entry — never send anyone here again
|
|
9152
|
+
this.router.navigate([fallback]);
|
|
8983
9153
|
}
|
|
8984
9154
|
// Explicit logoff drops only this user's entry — other users on the device keep theirs
|
|
8985
9155
|
async clear() {
|
|
@@ -8997,15 +9167,47 @@ class LastRouteService {
|
|
|
8997
9167
|
}
|
|
8998
9168
|
// Strip the leading slash and any query/fragment so what we store matches what router.navigate expects
|
|
8999
9169
|
normalize(url) {
|
|
9000
|
-
const route = (url
|
|
9170
|
+
const route = this.strip(url); // Changed: share the stripping with navigateTo's landed-URL check
|
|
9001
9171
|
return this.isResumable(route) ? route : null;
|
|
9002
9172
|
}
|
|
9173
|
+
// Added: url → bare route (no leading slash, no query, no fragment)
|
|
9174
|
+
strip(url) {
|
|
9175
|
+
return (url || '').split('?')[0].split('#')[0].replace(/^\//, '');
|
|
9176
|
+
}
|
|
9003
9177
|
// Only real, landable pages under home — never the bare shell, never a one-shot detour
|
|
9004
9178
|
isResumable(route) {
|
|
9005
9179
|
if (!route || !route.startsWith('home/'))
|
|
9006
9180
|
return false;
|
|
9007
9181
|
return !this.excluded.some(x => route.startsWith(x));
|
|
9008
9182
|
}
|
|
9183
|
+
// Added: the signed-in user's capability map, read from storage for the same reason as user() —
|
|
9184
|
+
// AuthService.myRole is still empty this early in a session resume (the landing page has no auth guard)
|
|
9185
|
+
async role() {
|
|
9186
|
+
try {
|
|
9187
|
+
return JSON.parse(await this.storage.get(Constants.AUTH_ROLES));
|
|
9188
|
+
}
|
|
9189
|
+
catch {
|
|
9190
|
+
return null;
|
|
9191
|
+
}
|
|
9192
|
+
}
|
|
9193
|
+
// Added: a route is resumable only while it still maps to a menu page this role can open. Retired pages lose
|
|
9194
|
+
// their cap item, revoked pages lose their role flag — both land the user on the default page instead.
|
|
9195
|
+
async allowed(route) {
|
|
9196
|
+
const items = this.flatten(this.dataService.appConfig?.capItems);
|
|
9197
|
+
if (!items.length)
|
|
9198
|
+
return true; // no menu configured — nothing to validate against, stay permissive
|
|
9199
|
+
const match = items
|
|
9200
|
+
.filter(x => x.link && x.link.length > 'home'.length && (route === x.link || route.startsWith(x.link + '/')))
|
|
9201
|
+
.sort((a, b) => b.link.length - a.link.length)[0]; // longest link wins: 'home/trips' beats the bare 'home'
|
|
9202
|
+
if (!match)
|
|
9203
|
+
return false; // not a menu destination (removed page, or a transient detour) — do not resume
|
|
9204
|
+
const role = await this.role();
|
|
9205
|
+
return role ? !!role[match.name] : true; // unreadable role must not disable the feature — the page still guards itself
|
|
9206
|
+
}
|
|
9207
|
+
// Added: cap items nest (group → sub items), so flatten the whole tree before matching a link
|
|
9208
|
+
flatten(items) {
|
|
9209
|
+
return (items || []).reduce((all, x) => all.concat(x, this.flatten(x.capSubItems)), []);
|
|
9210
|
+
}
|
|
9009
9211
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LastRouteService, deps: [{ token: i1$2.Router }, { token: StorageService }, { token: DataServiceLib }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
9010
9212
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LastRouteService, providedIn: 'root' }); }
|
|
9011
9213
|
}
|
|
@@ -10077,6 +10279,12 @@ class SelectCommonComponent {
|
|
|
10077
10279
|
if (this.masterField)
|
|
10078
10280
|
return;
|
|
10079
10281
|
const transformedAction = this.transformLoadUrl(loadAction);
|
|
10282
|
+
// Changed: a URL still holding an unresolved {placeholder} has no id to fetch against. Sending it asks
|
|
10283
|
+
// the server for the literal string, which 400s and pops an error dialog over the form the operator is
|
|
10284
|
+
// filling in. Wait for the parent field to be set - ngOnChanges re-fetches once the URL resolves.
|
|
10285
|
+
// Same rule the tab-count badges already follow, and for the same reason.
|
|
10286
|
+
if (transformedAction?.url?.includes('{'))
|
|
10287
|
+
return;
|
|
10080
10288
|
this.dataService.CallApi(transformedAction, "").subscribe((apiResponse) => {
|
|
10081
10289
|
if (apiResponse.success) {
|
|
10082
10290
|
// Always update options when new data arrives
|
|
@@ -12047,6 +12255,20 @@ class SelectComponent extends SelectCommonComponent {
|
|
|
12047
12255
|
this.getData(action);
|
|
12048
12256
|
}
|
|
12049
12257
|
}
|
|
12258
|
+
// Changed: re-fetch when a {placeholder} loadAction URL resolves to a different value (e.g. the customer changed on the load form).
|
|
12259
|
+
// resolveLoadAction memoizes on the resolved URL, so fields without a placeholder keep the same object and never reach the re-fetch.
|
|
12260
|
+
ngOnChanges(changes) {
|
|
12261
|
+
super.ngOnChanges();
|
|
12262
|
+
const change = changes?.['loadAction'];
|
|
12263
|
+
if (!change || change.firstChange || change.previousValue?.url === change.currentValue?.url)
|
|
12264
|
+
return;
|
|
12265
|
+
// Changed: the previous selection belonged to the previous parent, so it cannot stand once the parent changes
|
|
12266
|
+
if (this.value != null) {
|
|
12267
|
+
this.value = null;
|
|
12268
|
+
this.changed();
|
|
12269
|
+
}
|
|
12270
|
+
this.getData(change.currentValue);
|
|
12271
|
+
}
|
|
12050
12272
|
onHoverChange(isHovered) {
|
|
12051
12273
|
this.isHovered = isHovered;
|
|
12052
12274
|
}
|
|
@@ -12086,7 +12308,7 @@ class SelectComponent extends SelectCommonComponent {
|
|
|
12086
12308
|
}
|
|
12087
12309
|
}
|
|
12088
12310
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: DialogService }, { token: ButtonService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
12089
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SelectComponent, isStandalone: false, selector: "spa-select", inputs: { detailsConfig: "detailsConfig" }, usesInheritance: true, ngImport: i0, template: "<spa-select-common [width]=\"width\" [readonly]=\"readonly\" [required]=\"required\" [defaultFirstValue]=\"defaultFirstValue\"\n [readonlyMode]=\"readonlyMode\" [hint]=\"hint\" [placeholder]=\"placeholder\"\n [display]=\"display\" [(value)]=\"value\" [options]=\"options\" [masterOptions]=\"masterOptions\" [masterField]=\"masterField\"\n [optionValue]=\"optionValue\" [optionDisplay]=\"optionDisplay\" [optionDisplayExtra]=\"optionDisplayExtra\"\n [nullable]=\"nullable\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [loadAction]=\"loadAction\" [loadIDField]=\"loadIDField\" [field]=\"field\" [data]=\"data\"\n (valueChange)=\"valueChange.emit($event)\"\n (hoverChange)=\"onHoverChange($event)\">\n <ng-container additionalButtons>\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n\n </ng-container>\n</spa-select-common>\n", styles: [""], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SelectCommonComponent, selector: "spa-select-common", inputs: ["width", "readonly", "required", "defaultFirstValue", "readonlyMode", "hint", "placeholder", "display", "value", "options", "masterOptions", "masterField", "optionValue", "optionDisplay", "optionDisplayExtra", "nullable", "infoMessage", "copyContent", "loadAction", "loadIDField", "field", "data"], outputs: ["valueChange", "hoverChange"] }] }); }
|
|
12311
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SelectComponent, isStandalone: false, selector: "spa-select", inputs: { detailsConfig: "detailsConfig" }, usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<spa-select-common [width]=\"width\" [readonly]=\"readonly\" [required]=\"required\" [defaultFirstValue]=\"defaultFirstValue\"\n [readonlyMode]=\"readonlyMode\" [hint]=\"hint\" [placeholder]=\"placeholder\"\n [display]=\"display\" [(value)]=\"value\" [options]=\"options\" [masterOptions]=\"masterOptions\" [masterField]=\"masterField\"\n [optionValue]=\"optionValue\" [optionDisplay]=\"optionDisplay\" [optionDisplayExtra]=\"optionDisplayExtra\"\n [nullable]=\"nullable\" [infoMessage]=\"infoMessage\" [copyContent]=\"copyContent\" [loadAction]=\"loadAction\" [loadIDField]=\"loadIDField\" [field]=\"field\" [data]=\"data\"\n (valueChange)=\"valueChange.emit($event)\"\n (hoverChange)=\"onHoverChange($event)\">\n <ng-container additionalButtons>\n <button mat-icon-button *ngIf=\"detailsConfig && canCreate() && isHovered\" (click)=\"onPeekClick($event, 'create')\" matTooltip=\"Add\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" style=\"color: green;\">add</mat-icon>\n </button>\n <button mat-icon-button *ngIf=\"detailsConfig && canView() && isHovered && value\" (click)=\"onPeekClick($event, 'view')\" matTooltip=\"View\" matTooltipPosition=\"above\">\n <mat-icon class=\"tinyIcon\" color=\"primary\">launch</mat-icon>\n </button>\n\n </ng-container>\n</spa-select-common>\n", styles: [""], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SelectCommonComponent, selector: "spa-select-common", inputs: ["width", "readonly", "required", "defaultFirstValue", "readonlyMode", "hint", "placeholder", "display", "value", "options", "masterOptions", "masterField", "optionValue", "optionDisplay", "optionDisplayExtra", "nullable", "infoMessage", "copyContent", "loadAction", "loadIDField", "field", "data"], outputs: ["valueChange", "hoverChange"] }] }); }
|
|
12090
12312
|
}
|
|
12091
12313
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SelectComponent, decorators: [{
|
|
12092
12314
|
type: Component,
|
|
@@ -13190,6 +13412,7 @@ class TableComponent {
|
|
|
13190
13412
|
this.inTab = false;
|
|
13191
13413
|
this.nestingLevel = 0; // Changed: Track nesting depth for dialog recursion control
|
|
13192
13414
|
this.dataLoad = new EventEmitter();
|
|
13415
|
+
this.totalChange = new EventEmitter(); // Added: row total for the set this grid actually loaded — lets hosts (tab count badges) reuse the filtered result instead of firing a second, unfiltered count call
|
|
13193
13416
|
this.actionSuccess = new EventEmitter();
|
|
13194
13417
|
this.refreshClick = new EventEmitter();
|
|
13195
13418
|
this.searchClick = new EventEmitter();
|
|
@@ -13674,6 +13897,7 @@ class TableComponent {
|
|
|
13674
13897
|
}
|
|
13675
13898
|
dataLoaded(x) {
|
|
13676
13899
|
this.dataLoad.emit(x);
|
|
13900
|
+
this.totalChange.emit(this.pagedMode ? this.serverTotal : (Array.isArray(x) ? x.length : 0)); // Added: paged mode already knows the true server total; legacy mode loads the whole filtered set, so its length IS the total
|
|
13677
13901
|
if (this.config.tileConfig?.loadAction) {
|
|
13678
13902
|
this.tileReload.next(true);
|
|
13679
13903
|
}
|
|
@@ -14138,7 +14362,7 @@ class TableComponent {
|
|
|
14138
14362
|
this.setPaginator();
|
|
14139
14363
|
}
|
|
14140
14364
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: i1$3.BreakpointObserver }, { token: i4.MatDialog }, { token: ButtonService }, { token: DialogService }, { token: TableConfigService }, { token: ConditionService }, { token: AuthService }, { token: SignalRService }, { token: OfflineService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14141
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableComponent, isStandalone: false, selector: "spa-table", inputs: { data: "data", tileData: "tileData", config: "config", localMode: "localMode", parentDetails: "parentDetails", reload: "reload", activeTab: "activeTab", inTab: "inTab", nestingLevel: "nestingLevel" }, outputs: { dataLoad: "dataLoad", actionSuccess: "actionSuccess", refreshClick: "refreshClick", searchClick: "searchClick", createClick: "createClick", actionClick: "actionClick", inputChange: "inputChange", actionResponse: "actionResponse" }, viewQueries: [{ propertyName: "tablePaginator", first: true, predicate: ["tablePaginator"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "\n<ng-container *ngIf=\"hasFormAccess\">\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"config.realTime\" [isConnected]=\"isSignalRConnected\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far -->\n <div *ngIf=\"pagedMode && filterActive && loadedRows.length < serverTotal\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource\"><em>Loading...</em></p>\n\n <div *ngIf=\"dataSource && (!smallScreen || (smallScreen && dataSource?.length > 0))\">\n\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\">\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked(column, row)\" (showBannerEvent)=\"showBanner($event)\">\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row)\">\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0)}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0)}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row)\">\n </spa-capsules>\n\n\n <!-- Cards -->\n <spa-cards *ngIf=\"config?.viewType === 'card'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p>\n </div>\n\n</ng-container>\n\n\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i7.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i7.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i7.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i7.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i7.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i7.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i7.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i7.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i7.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i7.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i14$2.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SearchComponent, selector: "spa-search", inputs: ["config", "smallScreen", "tableDataSource"], outputs: ["searchClick"] }, { kind: "component", type: TableHeaderComponent, selector: "app-table-header", inputs: ["lastSearch", "config", "hideTitle", "tableDataSource", "tileConfig", "smallScreen", "tileReload", "showFilterButton", "data", "tileData", "isRealTime", "isConnected"], outputs: ["createClick", "customClick", "refreshClick", "tileClick", "tileUnClick", "filterChange"] }, { kind: "component", type: TableRowComponent, selector: "app-table-row", inputs: ["column", "row", "config", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: TableActionComponent, selector: "app-table-action", inputs: ["displayedButtons", "config", "row", "smallScreen"], outputs: ["actionClick"] }, { kind: "component", type: InlineCellComponent, selector: "app-inline-cell", inputs: ["field", "data"], outputs: ["valueChange"] }, { kind: "component", type: CapsulesComponent, selector: "spa-capsules", inputs: ["config", "dataSource", "displayedButtons"], outputs: ["actionClick"] }, { kind: "component", type: CardsComponent, selector: "spa-cards", inputs: ["config", "dataSource", "displayedButtons", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: GroupsComponent, selector: "spa-groups", inputs: ["config", "dataSource", "displayedButtons"], outputs: ["actionClick"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
14365
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TableComponent, isStandalone: false, selector: "spa-table", inputs: { data: "data", tileData: "tileData", config: "config", localMode: "localMode", parentDetails: "parentDetails", reload: "reload", activeTab: "activeTab", inTab: "inTab", nestingLevel: "nestingLevel" }, outputs: { dataLoad: "dataLoad", totalChange: "totalChange", actionSuccess: "actionSuccess", refreshClick: "refreshClick", searchClick: "searchClick", createClick: "createClick", actionClick: "actionClick", inputChange: "inputChange", actionResponse: "actionResponse" }, viewQueries: [{ propertyName: "tablePaginator", first: true, predicate: ["tablePaginator"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "\n<ng-container *ngIf=\"hasFormAccess\">\n\n <!-- Search -->\n <spa-search\n *ngIf=\"config.searchConfig\" [config]=\"config.searchConfig\" [smallScreen]=\"smallScreen\" [tableDataSource]=\"tableDataSource\" style=\"margin-bottom: 20px;\" (searchClick)=\"searchClicked($event)\">\n </spa-search>\n\n <!-- Header -->\n <app-table-header\n [config]=\"config\" [data]=\"dataSource\" [tableDataSource]=\"tableDataSource\" [tileConfig]=\"config.tileConfig\" [tileData]=\"tileData\" [tileReload]=\"tileReload\" [lastSearch]=\"lastSearch\" [smallScreen]=\"smallScreen\"\n [showFilterButton]=\"showFilterButton\" [isRealTime]=\"config.realTime\" [isConnected]=\"isSignalRConnected\"\n (createClick)=\"newModel()\" (customClick)=\"customModel($event,null)\"\n (refreshClick)=\"refreshClicked()\" (tileClick)=\"tileClicked($event)\" (tileUnClick)=\"tileUnClicked($event)\" (filterChange)=\"filterChanged($event)\">\n </app-table-header>\n\n <!-- Added: paged-mode filter hint \u2014 the client filter only covers rows loaded so far -->\n <div *ngIf=\"pagedMode && filterActive && loadedRows.length < serverTotal\" class=\"paged-filter-hint\">\n <mat-icon>info</mat-icon>\n <span>Filtering only the {{loadedRows.length}} loaded rows of {{serverTotal}}. {{ config.searchConfig ? 'Use Search for complete results.' : 'Refine with search for complete results.' }}</span>\n </div>\n\n\n <!-- Table -->\n <div *ngIf=\"!config.viewType || config?.viewType === 'table'\">\n\n <p *ngIf=\"!config\"><em>Configure Table</em></p>\n <p *ngIf=\"!dataSource\"><em>Loading...</em></p>\n\n <div *ngIf=\"dataSource && (!smallScreen || (smallScreen && dataSource?.length > 0))\">\n\n <table mat-table [dataSource]=\"tableDataSource\" [trackBy]=\"trackByRow\" [ngClass]=\"elevation\">\n\n <ng-container *ngFor=\"let column of config.columns\" [matColumnDef]=\"column.name\">\n <th mat-header-cell *matHeaderCellDef >{{ column.alias ?? column.name | camelToWords }}</th>\n <td mat-cell *matCellDef=\"let row;\" class=\"right-padding\" >\n\n <!-- Added: inline edit \u2014 editable cells swap to their form-field editor while the row is in edit mode -->\n <app-inline-cell *ngIf=\"isRowEditing(row) && getInlineField(column); else displayCell\" [field]=\"getInlineField(column)\" [data]=\"editingModel\"></app-inline-cell>\n\n <!-- Rows -->\n <ng-template #displayCell>\n <app-table-row [column]=\"column\" [row]=\"row\" [config]=\"config\" [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked(column.name, row)\" (columnClick)=\"columnClicked(column, row)\" (showBannerEvent)=\"showBanner($event)\">\n </app-table-row>\n </ng-template>\n\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"action\">\n <th mat-header-cell *matHeaderCellDef> Action </th>\n <td mat-cell *matCellDef=\"let row\" [ngStyle]=\"{width:false ? '20px' : actionsWidth}\">\n <div class=\"action-buttons-container\">\n\n <!-- Added: inline edit \u2014 while a row edits in place, its actions collapse to submit/cancel -->\n <ng-container *ngIf=\"isRowEditing(row); else rowActions\">\n <button mat-icon-button matTooltip=\"Save\" matTooltipPosition=\"above\" (click)=\"submitInlineEdit()\"><mat-icon class=\"inline-save\">check</mat-icon></button> <!-- Changed: dropped color=\"primary\" \u2014 the icon now carries a green save cue -->\n <button mat-icon-button matTooltip=\"Cancel\" matTooltipPosition=\"above\" (click)=\"cancelInlineEdit()\"><mat-icon class=\"inline-cancel\">close</mat-icon></button> <!-- Changed: red cancel cue -->\n </ng-container>\n\n <!-- Actions -->\n <ng-template #rowActions>\n <app-table-action\n [displayedButtons]=\"displayedButtons\" [config]=\"config\" [smallScreen]=\"smallScreen\" [row]=\"row\" (actionClick)=\"actionClicked($event.name, $event.row)\">\n </app-table-action>\n </ng-template>\n\n </div>\n </td>\n </ng-container>\n\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" [ngClass]=\"{'make-gray': (config.greyOut && config.greyOut(row)) || row.pendingApproval, 'row-editing': isRowEditing(row)}\"></tr> <!-- Changed: row-editing flags the row that is open for inline edit -->\n </table>\n\n </div>\n\n <!-- Changed: Removed *ngIf condition to keep paginator always in DOM and maintain ViewChild reference -->\n <!-- Changed: Added CSS class binding to hide when no data instead of conditional rendering -->\n <!-- Changed: Legacy paginator only renders in non-paged mode (pagedMode is constant per instance, set before first render) -->\n <mat-paginator *ngIf=\"!pagedMode\"\n #tablePaginator\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0)}\"\n showFirstLastButtons>\n </mat-paginator>\n\n <!-- Added: manual paginator for server-side paged mode \u2014 fully state-bound, never attached to MatTableDataSource. Always visible: when filtering it pages the in-memory filtered subset (length = filtered count); otherwise the server window (length = true total). No first/last jump (would force fetching the whole gap). -->\n <mat-paginator *ngIf=\"pagedMode\"\n [length]=\"filterActive ? filteredRows.length : serverTotal\"\n [pageIndex]=\"pageIndex\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"config.pageSizes ?? [10, 20, 50]\"\n [ngClass]=\"{'paginator-hidden': !dataSource || (smallScreen && dataSource?.length === 0)}\"\n (page)=\"onServerPage($event)\">\n </mat-paginator>\n\n </div>\n \n <!-- Capsules -->\n <spa-capsules *ngIf=\"config?.viewType === 'capsule'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row)\">\n </spa-capsules>\n\n\n <!-- Cards -->\n <spa-cards *ngIf=\"config?.viewType === 'card'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n [smallScreen]=\"smallScreen\"\n (actionClick)=\"actionClicked($event.name, $event.row)\"\n (columnClick)=\"columnClicked($event.column, $event.row)\"\n (showBannerEvent)=\"showBanner($event)\">\n </spa-cards>\n\n <!-- Groups - Added: New grouped view type -->\n <spa-groups *ngIf=\"config?.viewType === 'grouped'\"\n [config]=\"config\"\n [dataSource]=\"dataSource\"\n [displayedButtons]=\"displayedButtons\"\n (actionClick)=\"actionClicked($event.name, $event.row, $event.group, $event.button)\">\n </spa-groups>\n\n\n <div class=\"tin-center\">\n <p *ngIf=\"dataSource?.length == 0\"><em>{{config.noDataMessage ?? 'No Data'}}</em></p>\n </div>\n\n</ng-container>\n\n\n<ng-container *ngIf=\"!hasFormAccess\">\n <div class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n</ng-container>\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i7.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i7.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i7.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i7.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i7.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i7.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i7.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i7.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i7.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i7.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "component", type: i14$2.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SearchComponent, selector: "spa-search", inputs: ["config", "smallScreen", "tableDataSource"], outputs: ["searchClick"] }, { kind: "component", type: TableHeaderComponent, selector: "app-table-header", inputs: ["lastSearch", "config", "hideTitle", "tableDataSource", "tileConfig", "smallScreen", "tileReload", "showFilterButton", "data", "tileData", "isRealTime", "isConnected"], outputs: ["createClick", "customClick", "refreshClick", "tileClick", "tileUnClick", "filterChange"] }, { kind: "component", type: TableRowComponent, selector: "app-table-row", inputs: ["column", "row", "config", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: TableActionComponent, selector: "app-table-action", inputs: ["displayedButtons", "config", "row", "smallScreen"], outputs: ["actionClick"] }, { kind: "component", type: InlineCellComponent, selector: "app-inline-cell", inputs: ["field", "data"], outputs: ["valueChange"] }, { kind: "component", type: CapsulesComponent, selector: "spa-capsules", inputs: ["config", "dataSource", "displayedButtons"], outputs: ["actionClick"] }, { kind: "component", type: CardsComponent, selector: "spa-cards", inputs: ["config", "dataSource", "displayedButtons", "smallScreen"], outputs: ["actionClick", "columnClick", "showBannerEvent"] }, { kind: "component", type: GroupsComponent, selector: "spa-groups", inputs: ["config", "dataSource", "displayedButtons"], outputs: ["actionClick"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
14142
14366
|
}
|
|
14143
14367
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TableComponent, decorators: [{
|
|
14144
14368
|
type: Component,
|
|
@@ -14166,6 +14390,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
14166
14390
|
type: Input
|
|
14167
14391
|
}], dataLoad: [{
|
|
14168
14392
|
type: Output
|
|
14393
|
+
}], totalChange: [{
|
|
14394
|
+
type: Output
|
|
14169
14395
|
}], actionSuccess: [{
|
|
14170
14396
|
type: Output
|
|
14171
14397
|
}], refreshClick: [{
|
|
@@ -14237,7 +14463,7 @@ class SyncComponent {
|
|
|
14237
14463
|
this.alertConfig = messages.length ? { messages } : null;
|
|
14238
14464
|
}
|
|
14239
14465
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SyncComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14240
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SyncComponent, isStandalone: false, selector: "spa-sync", ngImport: i0, template: "<div class=\"row\">\n <div class=\"col-auto\">\n <h4>Pending Changes</h4>\n </div>\n</div>\n\n<hr style=\"margin-top: 0px;\" />\n\n<div style=\"font-size: 14px;\">\n <spa-alert *ngIf=\"alertConfig\" [config]=\"alertConfig\"></spa-alert>\n <spa-table [config]=\"tableConfig\" [data]=\"ops\"></spa-table>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: AlertComponent, selector: "spa-alert", inputs: ["config", "data"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
14466
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SyncComponent, isStandalone: false, selector: "spa-sync", ngImport: i0, template: "<div class=\"row\">\n <div class=\"col-auto\">\n <h4>Pending Changes</h4>\n </div>\n</div>\n\n<hr style=\"margin-top: 0px;\" />\n\n<div style=\"font-size: 14px;\">\n <spa-alert *ngIf=\"alertConfig\" [config]=\"alertConfig\"></spa-alert>\n <spa-table [config]=\"tableConfig\" [data]=\"ops\"></spa-table>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: AlertComponent, selector: "spa-alert", inputs: ["config", "data"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
14241
14467
|
}
|
|
14242
14468
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SyncComponent, decorators: [{
|
|
14243
14469
|
type: Component,
|
|
@@ -14906,6 +15132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
14906
15132
|
|
|
14907
15133
|
// Floating chat widget component for in-app Agent (renamed from AssistantComponent)
|
|
14908
15134
|
class AgentComponent {
|
|
15135
|
+
static { this.MAX_RECORD_SECONDS = 120; } // Auto-stop so a forgotten recording can't run away
|
|
14909
15136
|
constructor(agentService, signalRService) {
|
|
14910
15137
|
this.agentService = agentService;
|
|
14911
15138
|
this.signalRService = signalRService;
|
|
@@ -14916,9 +15143,20 @@ class AgentComponent {
|
|
|
14916
15143
|
this.greeting = '';
|
|
14917
15144
|
this.suggestedQuestions = [];
|
|
14918
15145
|
this.isOnline = false;
|
|
15146
|
+
this.isRecording = false; // Changed: voice note capture state
|
|
15147
|
+
this.recordSeconds = 0;
|
|
15148
|
+
this.mediaError = ''; // Changed: shown inline when the mic is blocked or unavailable
|
|
15149
|
+
this.pendingPhoto = null; // Changed: photo is staged, not sent — the user adds a caption first
|
|
15150
|
+
this.isTranscribing = false; // Changed: Stop pressed, waiting for the transcript to land in the text box
|
|
14919
15151
|
this.subs = [];
|
|
14920
15152
|
this.shouldScroll = false;
|
|
14921
15153
|
this.viewportHandler = null; // Changed: visualViewport resize handler reference
|
|
15154
|
+
// Changed: voice note recording internals
|
|
15155
|
+
this.recorder = null;
|
|
15156
|
+
this.mediaStream = null;
|
|
15157
|
+
this.chunks = [];
|
|
15158
|
+
this.recordTimer = null;
|
|
15159
|
+
this.stopMode = 'send'; // Changed: what to do with the clip once stopped
|
|
14922
15160
|
} // Changed: was assistantService
|
|
14923
15161
|
get agentName() {
|
|
14924
15162
|
return this.agentService.agentName;
|
|
@@ -14951,17 +15189,55 @@ class AgentComponent {
|
|
|
14951
15189
|
onDocumentClick(event) {
|
|
14952
15190
|
if (!this.isOpen)
|
|
14953
15191
|
return;
|
|
15192
|
+
// Changed: match on composedPath() instead of closest(). A control that re-renders the widget removes
|
|
15193
|
+
// itself from the DOM before this handler runs — the mic button lives inside *ngIf="!isRecording", so
|
|
15194
|
+
// starting a recording detaches it. closest() on a detached node finds nothing, and the widget used to
|
|
15195
|
+
// close itself the instant recording began, leaving the mic live behind a hidden window.
|
|
15196
|
+
//
|
|
15197
|
+
// cdk-overlay-container is checked for the same reason: Material renders menus, tooltips and dialogs in
|
|
15198
|
+
// an overlay attached to <body>, OUTSIDE .agent-window. Without this, picking an item from the attach
|
|
15199
|
+
// menu closed the whole chat. Any overlay open on top of the widget counts as being inside it.
|
|
15200
|
+
const path = (event.composedPath ? event.composedPath() : []);
|
|
15201
|
+
if (path.some(el => el?.classList?.contains('agent-window')
|
|
15202
|
+
|| el?.classList?.contains('agent-fab')
|
|
15203
|
+
|| el?.classList?.contains('cdk-overlay-container')))
|
|
15204
|
+
return;
|
|
15205
|
+
// Fallback for browsers without composedPath, and for targets still attached
|
|
14954
15206
|
const target = event.target;
|
|
14955
|
-
if (target.closest('.agent-window') || target.closest('.agent-fab'))
|
|
14956
|
-
return;
|
|
15207
|
+
if (target?.isConnected && (target.closest('.agent-window') || target.closest('.agent-fab')))
|
|
15208
|
+
return;
|
|
15209
|
+
// A detached target means the click came from a control that just re-rendered — i.e. inside the widget
|
|
15210
|
+
if (target && !target.isConnected)
|
|
15211
|
+
return;
|
|
14957
15212
|
this.agentService.close();
|
|
14958
15213
|
}
|
|
15214
|
+
// Changed: sends whatever is staged — typed text, an attached photo, or both. A recording in progress is
|
|
15215
|
+
// stopped and folded into the same send rather than being lost.
|
|
14959
15216
|
sendMessage() {
|
|
14960
|
-
if (
|
|
15217
|
+
if (this.isRecording) {
|
|
15218
|
+
this.stopMode = 'send';
|
|
15219
|
+
this.recorder?.stop();
|
|
15220
|
+
return;
|
|
15221
|
+
}
|
|
15222
|
+
const caption = this.inputText.trim();
|
|
15223
|
+
if (!caption && !this.pendingPhoto)
|
|
14961
15224
|
return;
|
|
14962
|
-
|
|
15225
|
+
if (this.pendingPhoto) {
|
|
15226
|
+
this.agentService.sendComposed(this.pendingPhoto, this.pendingPhoto.name || 'photo.jpg', null, '', caption);
|
|
15227
|
+
this.pendingPhoto = null;
|
|
15228
|
+
}
|
|
15229
|
+
else {
|
|
15230
|
+
this.agentService.sendMessage(caption);
|
|
15231
|
+
}
|
|
14963
15232
|
this.inputText = '';
|
|
14964
15233
|
}
|
|
15234
|
+
// Changed: true when there is anything worth sending
|
|
15235
|
+
get canSend() {
|
|
15236
|
+
return !!this.inputText.trim() || !!this.pendingPhoto;
|
|
15237
|
+
}
|
|
15238
|
+
clearPhoto() {
|
|
15239
|
+
this.pendingPhoto = null;
|
|
15240
|
+
}
|
|
14965
15241
|
sendSuggested(question) {
|
|
14966
15242
|
this.agentService.sendMessage(question);
|
|
14967
15243
|
}
|
|
@@ -14974,6 +15250,152 @@ class AgentComponent {
|
|
|
14974
15250
|
newConversation() {
|
|
14975
15251
|
this.agentService.newConversation();
|
|
14976
15252
|
}
|
|
15253
|
+
// Changed: mm:ss shown while recording
|
|
15254
|
+
get recordDisplay() {
|
|
15255
|
+
const mins = Math.floor(this.recordSeconds / 60);
|
|
15256
|
+
const secs = this.recordSeconds % 60;
|
|
15257
|
+
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
|
15258
|
+
}
|
|
15259
|
+
// Changed: voice note capture — the clip goes straight to the backend, which transcribes it and feeds the
|
|
15260
|
+
// text into the agent exactly as Telegram and WhatsApp voice notes are handled
|
|
15261
|
+
async startRecording() {
|
|
15262
|
+
if (this.isRecording)
|
|
15263
|
+
return;
|
|
15264
|
+
this.mediaError = '';
|
|
15265
|
+
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
|
|
15266
|
+
this.mediaError = 'Voice notes are not supported in this browser.';
|
|
15267
|
+
return;
|
|
15268
|
+
}
|
|
15269
|
+
try {
|
|
15270
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
15271
|
+
}
|
|
15272
|
+
catch {
|
|
15273
|
+
this.mediaError = 'Microphone access was blocked. Allow it in your browser settings to send voice notes.';
|
|
15274
|
+
return;
|
|
15275
|
+
}
|
|
15276
|
+
this.chunks = [];
|
|
15277
|
+
this.stopMode = 'send';
|
|
15278
|
+
const mimeType = this.pickAudioMimeType();
|
|
15279
|
+
this.recorder = mimeType ? new MediaRecorder(this.mediaStream, { mimeType }) : new MediaRecorder(this.mediaStream);
|
|
15280
|
+
this.recorder.ondataavailable = e => { if (e.data && e.data.size > 0)
|
|
15281
|
+
this.chunks.push(e.data); };
|
|
15282
|
+
this.recorder.onstop = () => this.finishRecording();
|
|
15283
|
+
this.recorder.start();
|
|
15284
|
+
this.isRecording = true;
|
|
15285
|
+
this.recordSeconds = 0;
|
|
15286
|
+
this.recordTimer = setInterval(() => {
|
|
15287
|
+
this.recordSeconds++;
|
|
15288
|
+
if (this.recordSeconds >= AgentComponent.MAX_RECORD_SECONDS)
|
|
15289
|
+
this.stopRecording();
|
|
15290
|
+
}, 1000);
|
|
15291
|
+
}
|
|
15292
|
+
// Changed: send immediately — the clip goes with whatever else is staged, no review step
|
|
15293
|
+
stopRecording() {
|
|
15294
|
+
if (!this.isRecording || !this.recorder)
|
|
15295
|
+
return;
|
|
15296
|
+
this.stopMode = 'send';
|
|
15297
|
+
this.recorder.stop(); // finishRecording() runs from onstop
|
|
15298
|
+
}
|
|
15299
|
+
// Changed: stop and transcribe INTO the text box so the user can read it back, edit it, and send when ready
|
|
15300
|
+
stopAndTranscribe() {
|
|
15301
|
+
if (!this.isRecording || !this.recorder)
|
|
15302
|
+
return;
|
|
15303
|
+
this.stopMode = 'transcribe';
|
|
15304
|
+
this.recorder.stop();
|
|
15305
|
+
}
|
|
15306
|
+
cancelRecording() {
|
|
15307
|
+
if (!this.isRecording || !this.recorder)
|
|
15308
|
+
return;
|
|
15309
|
+
this.stopMode = 'cancel';
|
|
15310
|
+
this.recorder.stop();
|
|
15311
|
+
}
|
|
15312
|
+
// Changed: "Take photo" — hands off to the device camera. On a phone this opens the camera app on the rear
|
|
15313
|
+
// lens and returns the shot; no in-page camera permission is involved because the OS handles it. Desktop
|
|
15314
|
+
// browsers ignore `capture` and fall back to the file picker.
|
|
15315
|
+
takePhoto() {
|
|
15316
|
+
this.cameraInput?.nativeElement.click();
|
|
15317
|
+
}
|
|
15318
|
+
// Changed: "Choose photo" — same input minus `capture`, so it opens the gallery / file picker
|
|
15319
|
+
choosePhoto() {
|
|
15320
|
+
this.galleryInput?.nativeElement.click();
|
|
15321
|
+
}
|
|
15322
|
+
// Changed: photo capture now STAGES the file instead of sending it. Nothing leaves until the user presses
|
|
15323
|
+
// send, so they can add a typed or spoken caption first — that caption is the actual command, the photo is
|
|
15324
|
+
// the evidence it refers to. `capture` opens the camera directly on mobile, the file picker on desktop.
|
|
15325
|
+
onPhotoSelected(event) {
|
|
15326
|
+
const input = event.target;
|
|
15327
|
+
const file = input.files && input.files.length ? input.files[0] : null;
|
|
15328
|
+
input.value = ''; // Reset so picking the same file twice still fires a change event
|
|
15329
|
+
if (!file)
|
|
15330
|
+
return;
|
|
15331
|
+
this.pendingPhoto = file;
|
|
15332
|
+
this.focusInput();
|
|
15333
|
+
}
|
|
15334
|
+
// Picks a container the browser can record AND the transcription endpoint accepts. Chrome/Edge/Firefox give
|
|
15335
|
+
// webm/opus, Safari gives mp4 — both are supported server-side, so no transcoding is needed anywhere.
|
|
15336
|
+
pickAudioMimeType() {
|
|
15337
|
+
const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/ogg;codecs=opus', 'audio/ogg'];
|
|
15338
|
+
return candidates.find(t => MediaRecorder.isTypeSupported(t)) || '';
|
|
15339
|
+
}
|
|
15340
|
+
finishRecording() {
|
|
15341
|
+
const mode = this.stopMode;
|
|
15342
|
+
this.clearRecording();
|
|
15343
|
+
if (mode === 'cancel' || !this.chunks.length) {
|
|
15344
|
+
this.chunks = [];
|
|
15345
|
+
return;
|
|
15346
|
+
}
|
|
15347
|
+
const type = this.recorder?.mimeType || 'audio/webm';
|
|
15348
|
+
const blob = new Blob(this.chunks, { type });
|
|
15349
|
+
const fileName = `voice-note.${this.extensionFor(type)}`;
|
|
15350
|
+
this.chunks = [];
|
|
15351
|
+
// Stop → transcribe into the text box for review; the photo (if any) stays staged
|
|
15352
|
+
if (mode === 'transcribe') {
|
|
15353
|
+
this.isTranscribing = true;
|
|
15354
|
+
this.agentService.transcribeOnly(blob, fileName).subscribe({
|
|
15355
|
+
next: (res) => {
|
|
15356
|
+
this.isTranscribing = false;
|
|
15357
|
+
if (res?.success && res.data?.text) {
|
|
15358
|
+
const existing = this.inputText.trim();
|
|
15359
|
+
this.inputText = existing ? `${existing} ${res.data.text}` : res.data.text;
|
|
15360
|
+
this.focusInput();
|
|
15361
|
+
}
|
|
15362
|
+
else {
|
|
15363
|
+
this.mediaError = res?.message || 'Sorry, that recording could not be transcribed.';
|
|
15364
|
+
}
|
|
15365
|
+
},
|
|
15366
|
+
error: () => {
|
|
15367
|
+
this.isTranscribing = false;
|
|
15368
|
+
this.mediaError = 'Sorry, that recording could not be transcribed.';
|
|
15369
|
+
}
|
|
15370
|
+
});
|
|
15371
|
+
return;
|
|
15372
|
+
}
|
|
15373
|
+
// Send → the clip goes with the staged photo and whatever is typed, all in one message
|
|
15374
|
+
this.agentService.sendComposed(this.pendingPhoto, this.pendingPhoto?.name || 'photo.jpg', blob, fileName, this.inputText.trim());
|
|
15375
|
+
this.pendingPhoto = null;
|
|
15376
|
+
this.inputText = '';
|
|
15377
|
+
}
|
|
15378
|
+
extensionFor(mimeType) {
|
|
15379
|
+
const type = (mimeType || '').toLowerCase();
|
|
15380
|
+
if (type.indexOf('mp4') >= 0)
|
|
15381
|
+
return 'mp4';
|
|
15382
|
+
if (type.indexOf('ogg') >= 0)
|
|
15383
|
+
return 'ogg';
|
|
15384
|
+
return 'webm';
|
|
15385
|
+
}
|
|
15386
|
+
// Releases the mic — without this the browser keeps showing the recording indicator
|
|
15387
|
+
clearRecording() {
|
|
15388
|
+
this.isRecording = false;
|
|
15389
|
+
this.recordSeconds = 0;
|
|
15390
|
+
if (this.recordTimer) {
|
|
15391
|
+
clearInterval(this.recordTimer);
|
|
15392
|
+
this.recordTimer = null;
|
|
15393
|
+
}
|
|
15394
|
+
if (this.mediaStream) {
|
|
15395
|
+
this.mediaStream.getTracks().forEach(t => t.stop());
|
|
15396
|
+
this.mediaStream = null;
|
|
15397
|
+
}
|
|
15398
|
+
}
|
|
14977
15399
|
scrollToBottom() {
|
|
14978
15400
|
try {
|
|
14979
15401
|
if (this.messageContainer) {
|
|
@@ -15005,12 +15427,15 @@ class AgentComponent {
|
|
|
15005
15427
|
}
|
|
15006
15428
|
ngOnDestroy() {
|
|
15007
15429
|
this.subs.forEach(s => s.unsubscribe());
|
|
15430
|
+
if (this.isRecording)
|
|
15431
|
+
this.cancelRecording(); // Changed: never leave the mic open behind a destroyed widget
|
|
15432
|
+
this.clearRecording();
|
|
15008
15433
|
if (this.viewportHandler && window.visualViewport) {
|
|
15009
15434
|
window.visualViewport.removeEventListener('resize', this.viewportHandler);
|
|
15010
15435
|
}
|
|
15011
15436
|
}
|
|
15012
15437
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgentComponent, deps: [{ token: AgentService }, { token: SignalRService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15013
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AgentComponent, isStandalone: false, selector: "spa-agent", host: { listeners: { "document:click": "onDocumentClick($event)" } }, viewQueries: [{ propertyName: "messageContainer", first: true, predicate: ["messageContainer"], descendants: true }, { propertyName: "messageInput", first: true, predicate: ["messageInput"], descendants: true }], ngImport: i0, template: "<!-- Floating chat widget for in-app Agent (renamed from Assistant) -->\n\n<!-- FAB toggle button -->\n<button mat-fab class=\"agent-fab\" (click)=\"toggleChat()\" [matTooltip]=\"agentName\">\n <mat-icon>{{ isOpen ? 'close' : 'chat_bubble' }}</mat-icon>\n</button>\n\n<!-- Chat window -->\n<div class=\"agent-window\" *ngIf=\"isOpen\" @slideUp>\n\n <!-- Header with online status -->\n <div class=\"agent-header\">\n <div class=\"header-info\">\n <mat-icon class=\"header-icon\">smart_toy</mat-icon>\n <div class=\"header-text\">\n <span class=\"header-title\">{{ agentName }}</span>\n <span class=\"status-badge\" [class.online]=\"isOnline\" [class.offline]=\"!isOnline\">\n <span class=\"status-dot\"></span>\n {{ isOnline ? 'Online' : 'Offline' }}\n </span>\n </div>\n </div>\n <div class=\"header-actions\">\n <button mat-icon-button matTooltip=\"New conversation\" (click)=\"newConversation()\">\n <mat-icon>add_comment</mat-icon>\n </button>\n <button mat-icon-button matTooltip=\"Close\" (click)=\"toggleChat()\">\n <mat-icon>remove</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Messages area -->\n <div class=\"agent-messages\" #messageContainer>\n\n <!-- Welcome experience (show when no messages) -->\n <div class=\"agent-greeting\" *ngIf=\"messages.length === 0\">\n <mat-icon class=\"greeting-icon\">smart_toy</mat-icon>\n <p class=\"greeting-text\">{{ greeting }}</p>\n\n <!-- Three capability pillars -->\n <div class=\"capability-pillars\">\n\n <!-- Pillar 1: Navigate & Inquire -->\n <div class=\"pillar\" (click)=\"sendSuggested('How do I post a transaction?')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">explore</mat-icon>\n <span class=\"pillar-title\">Ask & Navigate</span>\n </div>\n <p class=\"pillar-desc\">Ask how to do things or find features in the app</p>\n <span class=\"pillar-example\">\"How do I post a transaction?\"</span>\n </div>\n\n <!-- Pillar 2: Create & Modify -->\n <div class=\"pillar\" (click)=\"sendSuggested('Create a new customer')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">edit_note</mat-icon>\n <span class=\"pillar-title\">Create & Modify</span>\n </div>\n <p class=\"pillar-desc\">Add, edit, or delete records using natural language</p>\n <span class=\"pillar-example\">\"Create a new customer called Acme Ltd\"</span>\n </div>\n\n <!-- Pillar 3: Get Information -->\n <div class=\"pillar\" (click)=\"sendSuggested('List all accounts')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">search</mat-icon>\n <span class=\"pillar-title\">Get Information</span>\n </div>\n <p class=\"pillar-desc\">Retrieve details, list records, or get summaries</p>\n <span class=\"pillar-example\">\"Show me the details of Customer A\"</span>\n </div>\n\n </div>\n </div>\n\n <!-- Message bubbles -->\n <div *ngFor=\"let msg of messages; trackBy: trackMessage\" class=\"message-row\" [class.user-row]=\"msg.role === 'user'\" [class.agent-row]=\"msg.role === 'assistant'\">\n <div class=\"message-bubble\" [class.user-bubble]=\"msg.role === 'user'\" [class.agent-bubble]=\"msg.role === 'assistant'\">\n {{ msg.content }}\n </div>\n </div>\n\n <!-- Typing indicator -->\n <div class=\"message-row agent-row\" *ngIf=\"isTyping\">\n <div class=\"message-bubble agent-bubble typing-bubble\">\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n </div>\n </div>\n\n </div>\n\n <!-- Input area -->\n <div class=\"agent-input\">\n <mat-form-field appearance=\"outline\" class=\"input-field\">\n <input matInput #messageInput placeholder=\"Type a message...\" [(ngModel)]=\"inputText\" (keydown)=\"onKeydown($event)\" autocomplete=\"off\" />\n </mat-form-field>\n <button mat-icon-button color=\"primary\" class=\"send-btn\" (click)=\"sendMessage()\" [disabled]=\"!inputText.trim()\">\n <mat-icon>send</mat-icon>\n </button>\n </div>\n\n</div>\n", styles: [".agent-fab{position:fixed;bottom:24px;right:24px;z-index:1001;background:#1976d2;color:#fff}.agent-window{position:fixed;bottom:96px;right:24px;width:400px;height:580px;background:#fff;border-radius:16px;box-shadow:0 8px 32px #00000026;display:flex;flex-direction:column;z-index:1000;overflow:hidden}.agent-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#1976d2;color:#fff;min-height:56px}.header-info{display:flex;align-items:center;gap:10px}.header-icon{font-size:24px;width:24px;height:24px}.header-text{display:flex;flex-direction:column;gap:1px}.header-title{font-size:16px;font-weight:500;line-height:1.2}.status-badge{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:400;opacity:.9;line-height:1}.status-dot{width:7px;height:7px;border-radius:50%;display:inline-block}.status-badge.online .status-dot{background:#4caf50;box-shadow:0 0 4px #4caf5099}.status-badge.offline .status-dot{background:#9e9e9e}.header-actions{display:flex;gap:0}.header-actions button{color:#fff}.agent-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px}.agent-greeting{display:flex;flex-direction:column;align-items:center;text-align:center;padding:20px 8px 8px;gap:10px}.greeting-icon{font-size:48px;width:48px;height:48px;color:#1976d2;opacity:.8}.greeting-text{font-size:15px;color:#424242;line-height:1.5;margin:0}.capability-pillars{display:flex;flex-direction:column;gap:10px;margin-top:8px;width:100%}.pillar{border:1px solid #e0e0e0;border-radius:12px;padding:12px 14px;text-align:left;cursor:pointer;transition:all .2s ease;background:#fafafa}.pillar:hover{border-color:#1976d2;background:#1976d20a;box-shadow:0 2px 8px #1976d21a}.pillar-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.pillar-icon{font-size:20px;width:20px;height:20px;color:#1976d2}.pillar-title{font-size:13px;font-weight:600;color:#212121}.pillar-desc{font-size:12px;color:#616161;margin:0 0 6px;line-height:1.4}.pillar-example{font-size:12px;color:#1976d2;font-style:italic;opacity:.85}.message-row{display:flex;max-width:85%}.user-row{align-self:flex-end}.agent-row{align-self:flex-start}.message-bubble{padding:10px 14px;border-radius:16px;font-size:14px;line-height:1.5;word-break:break-word;white-space:pre-wrap}.user-bubble{background:#1976d2;color:#fff;border-bottom-right-radius:4px}.agent-bubble{background:#f0f0f0;color:#212121;border-bottom-left-radius:4px}.typing-bubble{display:flex;align-items:center;gap:4px;padding:12px 18px}.typing-dot{width:8px;height:8px;border-radius:50%;background:#9e9e9e;animation:typingBounce 1.4s infinite ease-in-out}.typing-dot:nth-child(2){animation-delay:.2s}.typing-dot:nth-child(3){animation-delay:.4s}@keyframes typingBounce{0%,60%,to{transform:translateY(0);opacity:.4}30%{transform:translateY(-6px);opacity:1}}.agent-input{display:flex;align-items:center;padding:8px 12px;border-top:1px solid #e0e0e0;gap:4px}.input-field{flex:1}.input-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.input-field ::ng-deep .mat-mdc-text-field-wrapper{padding:0 12px}.send-btn{margin-bottom:4px}@media (max-width: 600px){.agent-window{inset:0 0 auto;width:100%;height:100%;height:100dvh;height:var(--agent-vh, 100dvh);border-radius:0}.agent-fab{bottom:72px;right:16px}}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: i7$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], animations: [
|
|
15438
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AgentComponent, isStandalone: false, selector: "spa-agent", host: { listeners: { "document:click": "onDocumentClick($event)" } }, viewQueries: [{ propertyName: "messageContainer", first: true, predicate: ["messageContainer"], descendants: true }, { propertyName: "messageInput", first: true, predicate: ["messageInput"], descendants: true }, { propertyName: "cameraInput", first: true, predicate: ["cameraInput"], descendants: true }, { propertyName: "galleryInput", first: true, predicate: ["galleryInput"], descendants: true }], ngImport: i0, template: "<!-- Floating chat widget for in-app Agent (renamed from Assistant) -->\n\n<!-- FAB toggle button -->\n<button mat-fab class=\"agent-fab\" (click)=\"toggleChat()\" [matTooltip]=\"agentName\">\n <mat-icon>{{ isOpen ? 'close' : 'chat_bubble' }}</mat-icon>\n</button>\n\n<!-- Chat window -->\n<div class=\"agent-window\" *ngIf=\"isOpen\" @slideUp>\n\n <!-- Header with online status -->\n <div class=\"agent-header\">\n <div class=\"header-info\">\n <mat-icon class=\"header-icon\">smart_toy</mat-icon>\n <div class=\"header-text\">\n <span class=\"header-title\">{{ agentName }}</span>\n <span class=\"status-badge\" [class.online]=\"isOnline\" [class.offline]=\"!isOnline\">\n <span class=\"status-dot\"></span>\n {{ isOnline ? 'Online' : 'Offline' }}\n </span>\n </div>\n </div>\n <div class=\"header-actions\">\n <button mat-icon-button matTooltip=\"New conversation\" (click)=\"newConversation()\">\n <mat-icon>add_comment</mat-icon>\n </button>\n <button mat-icon-button matTooltip=\"Close\" (click)=\"toggleChat()\">\n <mat-icon>remove</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Messages area -->\n <div class=\"agent-messages\" #messageContainer>\n\n <!-- Welcome experience (show when no messages) -->\n <div class=\"agent-greeting\" *ngIf=\"messages.length === 0\">\n <mat-icon class=\"greeting-icon\">smart_toy</mat-icon>\n <p class=\"greeting-text\">{{ greeting }}</p>\n\n <!-- Three capability pillars -->\n <div class=\"capability-pillars\">\n\n <!-- Pillar 1: Navigate & Inquire -->\n <div class=\"pillar\" (click)=\"sendSuggested('How do I post a transaction?')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">explore</mat-icon>\n <span class=\"pillar-title\">Ask & Navigate</span>\n </div>\n <p class=\"pillar-desc\">Ask how to do things or find features in the app</p>\n <span class=\"pillar-example\">\"How do I post a transaction?\"</span>\n </div>\n\n <!-- Pillar 2: Create & Modify -->\n <div class=\"pillar\" (click)=\"sendSuggested('Create a new customer')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">edit_note</mat-icon>\n <span class=\"pillar-title\">Create & Modify</span>\n </div>\n <p class=\"pillar-desc\">Add, edit, or delete records using natural language</p>\n <span class=\"pillar-example\">\"Create a new customer called Acme Ltd\"</span>\n </div>\n\n <!-- Pillar 3: Get Information -->\n <div class=\"pillar\" (click)=\"sendSuggested('List all accounts')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">search</mat-icon>\n <span class=\"pillar-title\">Get Information</span>\n </div>\n <p class=\"pillar-desc\">Retrieve details, list records, or get summaries</p>\n <span class=\"pillar-example\">\"Show me the details of Customer A\"</span>\n </div>\n\n </div>\n </div>\n\n <!-- Message bubbles -->\n <div *ngFor=\"let msg of messages; trackBy: trackMessage\" class=\"message-row\" [class.user-row]=\"msg.role === 'user'\" [class.agent-row]=\"msg.role === 'assistant'\">\n <div class=\"message-bubble\" [class.user-bubble]=\"msg.role === 'user'\" [class.agent-bubble]=\"msg.role === 'assistant'\" [class.pending-bubble]=\"msg.pending\">\n {{ msg.content }}\n </div>\n </div>\n\n <!-- Typing indicator -->\n <div class=\"message-row agent-row\" *ngIf=\"isTyping\">\n <div class=\"message-bubble agent-bubble typing-bubble\">\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n </div>\n </div>\n\n </div>\n\n <!-- Media error (mic blocked / unsupported browser) -->\n <div class=\"agent-media-error\" *ngIf=\"mediaError\">\n <mat-icon class=\"media-error-icon\">error_outline</mat-icon>\n <span>{{ mediaError }}</span>\n </div>\n\n <!-- Staged attachment: a photo waits here until the user adds a caption and presses send -->\n <div class=\"agent-attachment\" *ngIf=\"pendingPhoto\">\n <mat-icon class=\"attach-icon\">image</mat-icon>\n <span class=\"attach-label\">Photo attached</span>\n <button mat-icon-button class=\"attach-remove\" matTooltip=\"Remove photo\" (click)=\"clearPhoto()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <!-- Transcribing indicator for the Stop button -->\n <div class=\"agent-transcribing\" *ngIf=\"isTranscribing\">\n <mat-icon class=\"transcribing-icon\">graphic_eq</mat-icon>\n <span>Transcribing\u2026</span>\n </div>\n\n <!-- Input area -->\n <div class=\"agent-input\" [class.recording]=\"isRecording\">\n\n <!-- Normal input: attach, text, voice note, send -->\n <ng-container *ngIf=\"!isRecording\">\n <button mat-icon-button class=\"media-btn\" matTooltip=\"Attach\" [matMenuTriggerFor]=\"attachMenu\">\n <mat-icon>add</mat-icon>\n </button>\n <mat-form-field appearance=\"outline\" class=\"input-field\">\n <input matInput #messageInput placeholder=\"Type, speak or snap...\" [(ngModel)]=\"inputText\" (keydown)=\"onKeydown($event)\" autocomplete=\"off\" />\n </mat-form-field>\n <button mat-icon-button class=\"media-btn\" matTooltip=\"Record a voice note\" (click)=\"startRecording()\">\n <mat-icon>mic</mat-icon>\n </button>\n <button mat-icon-button color=\"primary\" class=\"send-btn\" (click)=\"sendMessage()\" [disabled]=\"!canSend\">\n <mat-icon>send</mat-icon>\n </button>\n </ng-container>\n\n <!-- Recording bar: replaces the input while a voice note is being captured.\n Three exits \u2014 discard it, stop and read it back as text, or send it straight away. -->\n <ng-container *ngIf=\"isRecording\">\n <button mat-icon-button class=\"media-btn cancel-btn\" matTooltip=\"Discard\" (click)=\"cancelRecording()\">\n <mat-icon>delete</mat-icon>\n </button>\n <div class=\"recording-status\">\n <span class=\"rec-dot\"></span>\n <span class=\"rec-label\">Recording</span>\n <span class=\"rec-time\">{{ recordDisplay }}</span>\n </div>\n <button mat-icon-button class=\"media-btn stop-btn\" matTooltip=\"Stop and add as text\" (click)=\"stopAndTranscribe()\">\n <mat-icon>stop_circle</mat-icon>\n </button>\n <button mat-icon-button color=\"primary\" class=\"send-btn\" matTooltip=\"Send now\" (click)=\"stopRecording()\">\n <mat-icon>send</mat-icon>\n </button>\n </ng-container>\n\n <!-- Attach menu \u2014 keeps the row compact: one \"+\" instead of a button per source -->\n <mat-menu #attachMenu=\"matMenu\">\n <button mat-menu-item (click)=\"takePhoto()\">\n <mat-icon>photo_camera</mat-icon>\n <span>Take photo</span>\n </button>\n <button mat-menu-item (click)=\"choosePhoto()\">\n <mat-icon>photo_library</mat-icon>\n <span>Choose photo</span>\n </button>\n </mat-menu>\n\n <!-- Two separate pickers on purpose. `capture=\"environment\"` hands off to the phone's camera app and\n opens the rear lens; WITHOUT it the same input opens the gallery/file picker. One input cannot do\n both, which is why the old single button always went to the camera on mobile. -->\n <input type=\"file\" #cameraInput accept=\"image/*\" capture=\"environment\" hidden (change)=\"onPhotoSelected($event)\" />\n <input type=\"file\" #galleryInput accept=\"image/*\" hidden (change)=\"onPhotoSelected($event)\" />\n </div>\n\n</div>\n", styles: [".agent-fab{position:fixed;bottom:24px;right:24px;z-index:1001;background:#1976d2;color:#fff}.agent-window{position:fixed;bottom:96px;right:24px;width:400px;height:580px;background:#fff;border-radius:16px;box-shadow:0 8px 32px #00000026;display:flex;flex-direction:column;z-index:1000;overflow:hidden}.agent-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#1976d2;color:#fff;min-height:56px}.header-info{display:flex;align-items:center;gap:10px}.header-icon{font-size:24px;width:24px;height:24px}.header-text{display:flex;flex-direction:column;gap:1px}.header-title{font-size:16px;font-weight:500;line-height:1.2}.status-badge{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:400;opacity:.9;line-height:1}.status-dot{width:7px;height:7px;border-radius:50%;display:inline-block}.status-badge.online .status-dot{background:#4caf50;box-shadow:0 0 4px #4caf5099}.status-badge.offline .status-dot{background:#9e9e9e}.header-actions{display:flex;gap:0}.header-actions button{color:#fff}.agent-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px}.agent-greeting{display:flex;flex-direction:column;align-items:center;text-align:center;padding:20px 8px 8px;gap:10px}.greeting-icon{font-size:48px;width:48px;height:48px;color:#1976d2;opacity:.8}.greeting-text{font-size:15px;color:#424242;line-height:1.5;margin:0}.capability-pillars{display:flex;flex-direction:column;gap:10px;margin-top:8px;width:100%}.pillar{border:1px solid #e0e0e0;border-radius:12px;padding:12px 14px;text-align:left;cursor:pointer;transition:all .2s ease;background:#fafafa}.pillar:hover{border-color:#1976d2;background:#1976d20a;box-shadow:0 2px 8px #1976d21a}.pillar-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.pillar-icon{font-size:20px;width:20px;height:20px;color:#1976d2}.pillar-title{font-size:13px;font-weight:600;color:#212121}.pillar-desc{font-size:12px;color:#616161;margin:0 0 6px;line-height:1.4}.pillar-example{font-size:12px;color:#1976d2;font-style:italic;opacity:.85}.message-row{display:flex;max-width:85%}.user-row{align-self:flex-end}.agent-row{align-self:flex-start}.message-bubble{padding:10px 14px;border-radius:16px;font-size:14px;line-height:1.5;word-break:break-word;white-space:pre-wrap}.user-bubble{background:#1976d2;color:#fff;border-bottom-right-radius:4px}.agent-bubble{background:#f0f0f0;color:#212121;border-bottom-left-radius:4px}.typing-bubble{display:flex;align-items:center;gap:4px;padding:12px 18px}.typing-dot{width:8px;height:8px;border-radius:50%;background:#9e9e9e;animation:typingBounce 1.4s infinite ease-in-out}.typing-dot:nth-child(2){animation-delay:.2s}.typing-dot:nth-child(3){animation-delay:.4s}@keyframes typingBounce{0%,60%,to{transform:translateY(0);opacity:.4}30%{transform:translateY(-6px);opacity:1}}.agent-input{display:flex;align-items:center;padding:8px 12px;border-top:1px solid #e0e0e0;gap:4px}.input-field{flex:1}.input-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.input-field ::ng-deep .mat-mdc-text-field-wrapper{padding:0 12px}.send-btn,.media-btn{margin-bottom:0;display:inline-flex;align-items:center;justify-content:center}.media-btn{color:#757575}.media-btn:hover{color:#424242}.cancel-btn:hover{color:#d32f2f}.agent-input.recording{background:#fff5f5}.recording-status{flex:1;display:flex;align-items:center;gap:8px;padding:0 4px;font-size:13px;color:#616161}.rec-label{flex:1}.rec-time{font-variant-numeric:tabular-nums;color:#424242}.rec-dot{width:10px;height:10px;border-radius:50%;background:#e53935;animation:rec-pulse 1.2s ease-in-out infinite}@keyframes rec-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.85)}}.pending-bubble{opacity:.6;font-style:italic}.agent-attachment{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#1565c0;background:#e8f1fb;border-top:1px solid #cfe0f3}.attach-icon{font-size:18px;width:18px;height:18px}.attach-label{flex:1}.attach-remove{width:24px;height:24px;min-width:0;padding:0;flex:0 0 auto;line-height:24px;display:inline-flex;align-items:center;justify-content:center;color:#1565c0}.attach-remove .mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.agent-transcribing{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#616161;background:#f5f5f5;border-top:1px solid #e0e0e0}.transcribing-icon{font-size:18px;width:18px;height:18px;animation:rec-pulse 1.2s ease-in-out infinite}.stop-btn{color:#e53935}.agent-media-error{display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:12px;color:#c62828;background:#fdecea;border-top:1px solid #f5c6c2}.media-error-icon{font-size:18px;width:18px;height:18px}@media (max-width: 600px){.agent-window{inset:0 0 auto;width:100%;height:100%;height:100dvh;height:var(--agent-vh, 100dvh);border-radius:0}.agent-fab{bottom:72px;right:16px}}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4$4.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i4$4.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i4$4.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$3.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: i7$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], animations: [
|
|
15014
15439
|
trigger('slideUp', [
|
|
15015
15440
|
transition(':enter', [
|
|
15016
15441
|
style({ opacity: 0, transform: 'translateY(20px) scale(0.95)' }),
|
|
@@ -15034,13 +15459,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
15034
15459
|
animate('200ms cubic-bezier(0.4, 0, 0.2, 1)', style({ opacity: 0, transform: 'translateY(20px) scale(0.95)' }))
|
|
15035
15460
|
])
|
|
15036
15461
|
])
|
|
15037
|
-
], template: "<!-- Floating chat widget for in-app Agent (renamed from Assistant) -->\n\n<!-- FAB toggle button -->\n<button mat-fab class=\"agent-fab\" (click)=\"toggleChat()\" [matTooltip]=\"agentName\">\n <mat-icon>{{ isOpen ? 'close' : 'chat_bubble' }}</mat-icon>\n</button>\n\n<!-- Chat window -->\n<div class=\"agent-window\" *ngIf=\"isOpen\" @slideUp>\n\n <!-- Header with online status -->\n <div class=\"agent-header\">\n <div class=\"header-info\">\n <mat-icon class=\"header-icon\">smart_toy</mat-icon>\n <div class=\"header-text\">\n <span class=\"header-title\">{{ agentName }}</span>\n <span class=\"status-badge\" [class.online]=\"isOnline\" [class.offline]=\"!isOnline\">\n <span class=\"status-dot\"></span>\n {{ isOnline ? 'Online' : 'Offline' }}\n </span>\n </div>\n </div>\n <div class=\"header-actions\">\n <button mat-icon-button matTooltip=\"New conversation\" (click)=\"newConversation()\">\n <mat-icon>add_comment</mat-icon>\n </button>\n <button mat-icon-button matTooltip=\"Close\" (click)=\"toggleChat()\">\n <mat-icon>remove</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Messages area -->\n <div class=\"agent-messages\" #messageContainer>\n\n <!-- Welcome experience (show when no messages) -->\n <div class=\"agent-greeting\" *ngIf=\"messages.length === 0\">\n <mat-icon class=\"greeting-icon\">smart_toy</mat-icon>\n <p class=\"greeting-text\">{{ greeting }}</p>\n\n <!-- Three capability pillars -->\n <div class=\"capability-pillars\">\n\n <!-- Pillar 1: Navigate & Inquire -->\n <div class=\"pillar\" (click)=\"sendSuggested('How do I post a transaction?')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">explore</mat-icon>\n <span class=\"pillar-title\">Ask & Navigate</span>\n </div>\n <p class=\"pillar-desc\">Ask how to do things or find features in the app</p>\n <span class=\"pillar-example\">\"How do I post a transaction?\"</span>\n </div>\n\n <!-- Pillar 2: Create & Modify -->\n <div class=\"pillar\" (click)=\"sendSuggested('Create a new customer')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">edit_note</mat-icon>\n <span class=\"pillar-title\">Create & Modify</span>\n </div>\n <p class=\"pillar-desc\">Add, edit, or delete records using natural language</p>\n <span class=\"pillar-example\">\"Create a new customer called Acme Ltd\"</span>\n </div>\n\n <!-- Pillar 3: Get Information -->\n <div class=\"pillar\" (click)=\"sendSuggested('List all accounts')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">search</mat-icon>\n <span class=\"pillar-title\">Get Information</span>\n </div>\n <p class=\"pillar-desc\">Retrieve details, list records, or get summaries</p>\n <span class=\"pillar-example\">\"Show me the details of Customer A\"</span>\n </div>\n\n </div>\n </div>\n\n <!-- Message bubbles -->\n <div *ngFor=\"let msg of messages; trackBy: trackMessage\" class=\"message-row\" [class.user-row]=\"msg.role === 'user'\" [class.agent-row]=\"msg.role === 'assistant'\">\n <div class=\"message-bubble\" [class.user-bubble]=\"msg.role === 'user'\" [class.agent-bubble]=\"msg.role === 'assistant'\">\n {{ msg.content }}\n </div>\n </div>\n\n <!-- Typing indicator -->\n <div class=\"message-row agent-row\" *ngIf=\"isTyping\">\n <div class=\"message-bubble agent-bubble typing-bubble\">\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n </div>\n </div>\n\n </div>\n\n <!-- Input area -->\n <div class=\"agent-input\">\n <mat-form-field appearance=\"outline\" class=\"input-field\">\n <input matInput #messageInput placeholder=\"Type a message...\" [(ngModel)]=\"inputText\" (keydown)=\"onKeydown($event)\" autocomplete=\"off\" />\n </mat-form-field>\n <button mat-icon-button color=\"primary\" class=\"send-btn\" (click)=\"sendMessage()\" [disabled]=\"!inputText.trim()\">\n <mat-icon>send</mat-icon>\n </button>\n </div>\n\n</div>\n", styles: [".agent-fab{position:fixed;bottom:24px;right:24px;z-index:1001;background:#1976d2;color:#fff}.agent-window{position:fixed;bottom:96px;right:24px;width:400px;height:580px;background:#fff;border-radius:16px;box-shadow:0 8px 32px #00000026;display:flex;flex-direction:column;z-index:1000;overflow:hidden}.agent-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#1976d2;color:#fff;min-height:56px}.header-info{display:flex;align-items:center;gap:10px}.header-icon{font-size:24px;width:24px;height:24px}.header-text{display:flex;flex-direction:column;gap:1px}.header-title{font-size:16px;font-weight:500;line-height:1.2}.status-badge{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:400;opacity:.9;line-height:1}.status-dot{width:7px;height:7px;border-radius:50%;display:inline-block}.status-badge.online .status-dot{background:#4caf50;box-shadow:0 0 4px #4caf5099}.status-badge.offline .status-dot{background:#9e9e9e}.header-actions{display:flex;gap:0}.header-actions button{color:#fff}.agent-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px}.agent-greeting{display:flex;flex-direction:column;align-items:center;text-align:center;padding:20px 8px 8px;gap:10px}.greeting-icon{font-size:48px;width:48px;height:48px;color:#1976d2;opacity:.8}.greeting-text{font-size:15px;color:#424242;line-height:1.5;margin:0}.capability-pillars{display:flex;flex-direction:column;gap:10px;margin-top:8px;width:100%}.pillar{border:1px solid #e0e0e0;border-radius:12px;padding:12px 14px;text-align:left;cursor:pointer;transition:all .2s ease;background:#fafafa}.pillar:hover{border-color:#1976d2;background:#1976d20a;box-shadow:0 2px 8px #1976d21a}.pillar-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.pillar-icon{font-size:20px;width:20px;height:20px;color:#1976d2}.pillar-title{font-size:13px;font-weight:600;color:#212121}.pillar-desc{font-size:12px;color:#616161;margin:0 0 6px;line-height:1.4}.pillar-example{font-size:12px;color:#1976d2;font-style:italic;opacity:.85}.message-row{display:flex;max-width:85%}.user-row{align-self:flex-end}.agent-row{align-self:flex-start}.message-bubble{padding:10px 14px;border-radius:16px;font-size:14px;line-height:1.5;word-break:break-word;white-space:pre-wrap}.user-bubble{background:#1976d2;color:#fff;border-bottom-right-radius:4px}.agent-bubble{background:#f0f0f0;color:#212121;border-bottom-left-radius:4px}.typing-bubble{display:flex;align-items:center;gap:4px;padding:12px 18px}.typing-dot{width:8px;height:8px;border-radius:50%;background:#9e9e9e;animation:typingBounce 1.4s infinite ease-in-out}.typing-dot:nth-child(2){animation-delay:.2s}.typing-dot:nth-child(3){animation-delay:.4s}@keyframes typingBounce{0%,60%,to{transform:translateY(0);opacity:.4}30%{transform:translateY(-6px);opacity:1}}.agent-input{display:flex;align-items:center;padding:8px 12px;border-top:1px solid #e0e0e0;gap:4px}.input-field{flex:1}.input-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.input-field ::ng-deep .mat-mdc-text-field-wrapper{padding:0 12px}.send-btn{margin-bottom:4px}@media (max-width: 600px){.agent-window{inset:0 0 auto;width:100%;height:100%;height:100dvh;height:var(--agent-vh, 100dvh);border-radius:0}.agent-fab{bottom:72px;right:16px}}\n"] }]
|
|
15462
|
+
], template: "<!-- Floating chat widget for in-app Agent (renamed from Assistant) -->\n\n<!-- FAB toggle button -->\n<button mat-fab class=\"agent-fab\" (click)=\"toggleChat()\" [matTooltip]=\"agentName\">\n <mat-icon>{{ isOpen ? 'close' : 'chat_bubble' }}</mat-icon>\n</button>\n\n<!-- Chat window -->\n<div class=\"agent-window\" *ngIf=\"isOpen\" @slideUp>\n\n <!-- Header with online status -->\n <div class=\"agent-header\">\n <div class=\"header-info\">\n <mat-icon class=\"header-icon\">smart_toy</mat-icon>\n <div class=\"header-text\">\n <span class=\"header-title\">{{ agentName }}</span>\n <span class=\"status-badge\" [class.online]=\"isOnline\" [class.offline]=\"!isOnline\">\n <span class=\"status-dot\"></span>\n {{ isOnline ? 'Online' : 'Offline' }}\n </span>\n </div>\n </div>\n <div class=\"header-actions\">\n <button mat-icon-button matTooltip=\"New conversation\" (click)=\"newConversation()\">\n <mat-icon>add_comment</mat-icon>\n </button>\n <button mat-icon-button matTooltip=\"Close\" (click)=\"toggleChat()\">\n <mat-icon>remove</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Messages area -->\n <div class=\"agent-messages\" #messageContainer>\n\n <!-- Welcome experience (show when no messages) -->\n <div class=\"agent-greeting\" *ngIf=\"messages.length === 0\">\n <mat-icon class=\"greeting-icon\">smart_toy</mat-icon>\n <p class=\"greeting-text\">{{ greeting }}</p>\n\n <!-- Three capability pillars -->\n <div class=\"capability-pillars\">\n\n <!-- Pillar 1: Navigate & Inquire -->\n <div class=\"pillar\" (click)=\"sendSuggested('How do I post a transaction?')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">explore</mat-icon>\n <span class=\"pillar-title\">Ask & Navigate</span>\n </div>\n <p class=\"pillar-desc\">Ask how to do things or find features in the app</p>\n <span class=\"pillar-example\">\"How do I post a transaction?\"</span>\n </div>\n\n <!-- Pillar 2: Create & Modify -->\n <div class=\"pillar\" (click)=\"sendSuggested('Create a new customer')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">edit_note</mat-icon>\n <span class=\"pillar-title\">Create & Modify</span>\n </div>\n <p class=\"pillar-desc\">Add, edit, or delete records using natural language</p>\n <span class=\"pillar-example\">\"Create a new customer called Acme Ltd\"</span>\n </div>\n\n <!-- Pillar 3: Get Information -->\n <div class=\"pillar\" (click)=\"sendSuggested('List all accounts')\">\n <div class=\"pillar-header\">\n <mat-icon class=\"pillar-icon\">search</mat-icon>\n <span class=\"pillar-title\">Get Information</span>\n </div>\n <p class=\"pillar-desc\">Retrieve details, list records, or get summaries</p>\n <span class=\"pillar-example\">\"Show me the details of Customer A\"</span>\n </div>\n\n </div>\n </div>\n\n <!-- Message bubbles -->\n <div *ngFor=\"let msg of messages; trackBy: trackMessage\" class=\"message-row\" [class.user-row]=\"msg.role === 'user'\" [class.agent-row]=\"msg.role === 'assistant'\">\n <div class=\"message-bubble\" [class.user-bubble]=\"msg.role === 'user'\" [class.agent-bubble]=\"msg.role === 'assistant'\" [class.pending-bubble]=\"msg.pending\">\n {{ msg.content }}\n </div>\n </div>\n\n <!-- Typing indicator -->\n <div class=\"message-row agent-row\" *ngIf=\"isTyping\">\n <div class=\"message-bubble agent-bubble typing-bubble\">\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n <span class=\"typing-dot\"></span>\n </div>\n </div>\n\n </div>\n\n <!-- Media error (mic blocked / unsupported browser) -->\n <div class=\"agent-media-error\" *ngIf=\"mediaError\">\n <mat-icon class=\"media-error-icon\">error_outline</mat-icon>\n <span>{{ mediaError }}</span>\n </div>\n\n <!-- Staged attachment: a photo waits here until the user adds a caption and presses send -->\n <div class=\"agent-attachment\" *ngIf=\"pendingPhoto\">\n <mat-icon class=\"attach-icon\">image</mat-icon>\n <span class=\"attach-label\">Photo attached</span>\n <button mat-icon-button class=\"attach-remove\" matTooltip=\"Remove photo\" (click)=\"clearPhoto()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <!-- Transcribing indicator for the Stop button -->\n <div class=\"agent-transcribing\" *ngIf=\"isTranscribing\">\n <mat-icon class=\"transcribing-icon\">graphic_eq</mat-icon>\n <span>Transcribing\u2026</span>\n </div>\n\n <!-- Input area -->\n <div class=\"agent-input\" [class.recording]=\"isRecording\">\n\n <!-- Normal input: attach, text, voice note, send -->\n <ng-container *ngIf=\"!isRecording\">\n <button mat-icon-button class=\"media-btn\" matTooltip=\"Attach\" [matMenuTriggerFor]=\"attachMenu\">\n <mat-icon>add</mat-icon>\n </button>\n <mat-form-field appearance=\"outline\" class=\"input-field\">\n <input matInput #messageInput placeholder=\"Type, speak or snap...\" [(ngModel)]=\"inputText\" (keydown)=\"onKeydown($event)\" autocomplete=\"off\" />\n </mat-form-field>\n <button mat-icon-button class=\"media-btn\" matTooltip=\"Record a voice note\" (click)=\"startRecording()\">\n <mat-icon>mic</mat-icon>\n </button>\n <button mat-icon-button color=\"primary\" class=\"send-btn\" (click)=\"sendMessage()\" [disabled]=\"!canSend\">\n <mat-icon>send</mat-icon>\n </button>\n </ng-container>\n\n <!-- Recording bar: replaces the input while a voice note is being captured.\n Three exits \u2014 discard it, stop and read it back as text, or send it straight away. -->\n <ng-container *ngIf=\"isRecording\">\n <button mat-icon-button class=\"media-btn cancel-btn\" matTooltip=\"Discard\" (click)=\"cancelRecording()\">\n <mat-icon>delete</mat-icon>\n </button>\n <div class=\"recording-status\">\n <span class=\"rec-dot\"></span>\n <span class=\"rec-label\">Recording</span>\n <span class=\"rec-time\">{{ recordDisplay }}</span>\n </div>\n <button mat-icon-button class=\"media-btn stop-btn\" matTooltip=\"Stop and add as text\" (click)=\"stopAndTranscribe()\">\n <mat-icon>stop_circle</mat-icon>\n </button>\n <button mat-icon-button color=\"primary\" class=\"send-btn\" matTooltip=\"Send now\" (click)=\"stopRecording()\">\n <mat-icon>send</mat-icon>\n </button>\n </ng-container>\n\n <!-- Attach menu \u2014 keeps the row compact: one \"+\" instead of a button per source -->\n <mat-menu #attachMenu=\"matMenu\">\n <button mat-menu-item (click)=\"takePhoto()\">\n <mat-icon>photo_camera</mat-icon>\n <span>Take photo</span>\n </button>\n <button mat-menu-item (click)=\"choosePhoto()\">\n <mat-icon>photo_library</mat-icon>\n <span>Choose photo</span>\n </button>\n </mat-menu>\n\n <!-- Two separate pickers on purpose. `capture=\"environment\"` hands off to the phone's camera app and\n opens the rear lens; WITHOUT it the same input opens the gallery/file picker. One input cannot do\n both, which is why the old single button always went to the camera on mobile. -->\n <input type=\"file\" #cameraInput accept=\"image/*\" capture=\"environment\" hidden (change)=\"onPhotoSelected($event)\" />\n <input type=\"file\" #galleryInput accept=\"image/*\" hidden (change)=\"onPhotoSelected($event)\" />\n </div>\n\n</div>\n", styles: [".agent-fab{position:fixed;bottom:24px;right:24px;z-index:1001;background:#1976d2;color:#fff}.agent-window{position:fixed;bottom:96px;right:24px;width:400px;height:580px;background:#fff;border-radius:16px;box-shadow:0 8px 32px #00000026;display:flex;flex-direction:column;z-index:1000;overflow:hidden}.agent-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#1976d2;color:#fff;min-height:56px}.header-info{display:flex;align-items:center;gap:10px}.header-icon{font-size:24px;width:24px;height:24px}.header-text{display:flex;flex-direction:column;gap:1px}.header-title{font-size:16px;font-weight:500;line-height:1.2}.status-badge{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:400;opacity:.9;line-height:1}.status-dot{width:7px;height:7px;border-radius:50%;display:inline-block}.status-badge.online .status-dot{background:#4caf50;box-shadow:0 0 4px #4caf5099}.status-badge.offline .status-dot{background:#9e9e9e}.header-actions{display:flex;gap:0}.header-actions button{color:#fff}.agent-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px}.agent-greeting{display:flex;flex-direction:column;align-items:center;text-align:center;padding:20px 8px 8px;gap:10px}.greeting-icon{font-size:48px;width:48px;height:48px;color:#1976d2;opacity:.8}.greeting-text{font-size:15px;color:#424242;line-height:1.5;margin:0}.capability-pillars{display:flex;flex-direction:column;gap:10px;margin-top:8px;width:100%}.pillar{border:1px solid #e0e0e0;border-radius:12px;padding:12px 14px;text-align:left;cursor:pointer;transition:all .2s ease;background:#fafafa}.pillar:hover{border-color:#1976d2;background:#1976d20a;box-shadow:0 2px 8px #1976d21a}.pillar-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.pillar-icon{font-size:20px;width:20px;height:20px;color:#1976d2}.pillar-title{font-size:13px;font-weight:600;color:#212121}.pillar-desc{font-size:12px;color:#616161;margin:0 0 6px;line-height:1.4}.pillar-example{font-size:12px;color:#1976d2;font-style:italic;opacity:.85}.message-row{display:flex;max-width:85%}.user-row{align-self:flex-end}.agent-row{align-self:flex-start}.message-bubble{padding:10px 14px;border-radius:16px;font-size:14px;line-height:1.5;word-break:break-word;white-space:pre-wrap}.user-bubble{background:#1976d2;color:#fff;border-bottom-right-radius:4px}.agent-bubble{background:#f0f0f0;color:#212121;border-bottom-left-radius:4px}.typing-bubble{display:flex;align-items:center;gap:4px;padding:12px 18px}.typing-dot{width:8px;height:8px;border-radius:50%;background:#9e9e9e;animation:typingBounce 1.4s infinite ease-in-out}.typing-dot:nth-child(2){animation-delay:.2s}.typing-dot:nth-child(3){animation-delay:.4s}@keyframes typingBounce{0%,60%,to{transform:translateY(0);opacity:.4}30%{transform:translateY(-6px);opacity:1}}.agent-input{display:flex;align-items:center;padding:8px 12px;border-top:1px solid #e0e0e0;gap:4px}.input-field{flex:1}.input-field ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.input-field ::ng-deep .mat-mdc-text-field-wrapper{padding:0 12px}.send-btn,.media-btn{margin-bottom:0;display:inline-flex;align-items:center;justify-content:center}.media-btn{color:#757575}.media-btn:hover{color:#424242}.cancel-btn:hover{color:#d32f2f}.agent-input.recording{background:#fff5f5}.recording-status{flex:1;display:flex;align-items:center;gap:8px;padding:0 4px;font-size:13px;color:#616161}.rec-label{flex:1}.rec-time{font-variant-numeric:tabular-nums;color:#424242}.rec-dot{width:10px;height:10px;border-radius:50%;background:#e53935;animation:rec-pulse 1.2s ease-in-out infinite}@keyframes rec-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.85)}}.pending-bubble{opacity:.6;font-style:italic}.agent-attachment{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#1565c0;background:#e8f1fb;border-top:1px solid #cfe0f3}.attach-icon{font-size:18px;width:18px;height:18px}.attach-label{flex:1}.attach-remove{width:24px;height:24px;min-width:0;padding:0;flex:0 0 auto;line-height:24px;display:inline-flex;align-items:center;justify-content:center;color:#1565c0}.attach-remove .mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.agent-transcribing{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#616161;background:#f5f5f5;border-top:1px solid #e0e0e0}.transcribing-icon{font-size:18px;width:18px;height:18px;animation:rec-pulse 1.2s ease-in-out infinite}.stop-btn{color:#e53935}.agent-media-error{display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:12px;color:#c62828;background:#fdecea;border-top:1px solid #f5c6c2}.media-error-icon{font-size:18px;width:18px;height:18px}@media (max-width: 600px){.agent-window{inset:0 0 auto;width:100%;height:100%;height:100dvh;height:var(--agent-vh, 100dvh);border-radius:0}.agent-fab{bottom:72px;right:16px}}\n"] }]
|
|
15038
15463
|
}], ctorParameters: () => [{ type: AgentService }, { type: SignalRService }], propDecorators: { messageContainer: [{
|
|
15039
15464
|
type: ViewChild,
|
|
15040
15465
|
args: ['messageContainer']
|
|
15041
15466
|
}], messageInput: [{
|
|
15042
15467
|
type: ViewChild,
|
|
15043
15468
|
args: ['messageInput']
|
|
15469
|
+
}], cameraInput: [{
|
|
15470
|
+
type: ViewChild,
|
|
15471
|
+
args: ['cameraInput']
|
|
15472
|
+
}], galleryInput: [{
|
|
15473
|
+
type: ViewChild,
|
|
15474
|
+
args: ['galleryInput']
|
|
15044
15475
|
}], onDocumentClick: [{
|
|
15045
15476
|
type: HostListener,
|
|
15046
15477
|
args: ['document:click', ['$event']]
|
|
@@ -15844,6 +16275,14 @@ class FormComponent {
|
|
|
15844
16275
|
loadChildMasterOptions(child) {
|
|
15845
16276
|
if (!child.loadAction)
|
|
15846
16277
|
return;
|
|
16278
|
+
// Changed: skip a URL that still holds an unresolved {placeholder}. ngOnInit calls this for EVERY field
|
|
16279
|
+
// carrying a loadAction, so a dependent field like the load form's Customer PO picker
|
|
16280
|
+
// ('customerpurchaseorders/open/{customerID}') was requested with the literal placeholder before any
|
|
16281
|
+
// customer had been chosen - a 400 that popped an error dialog over the form as it opened.
|
|
16282
|
+
// NOTE worth revisiting separately: this runs for every field, not only master/child ones, so every
|
|
16283
|
+
// select is fetched twice on open - once here and once by the select itself.
|
|
16284
|
+
if (child.loadAction.url?.includes('{'))
|
|
16285
|
+
return;
|
|
15847
16286
|
this.dataService.CallApi(child.loadAction).subscribe((apiResponse) => {
|
|
15848
16287
|
if (apiResponse.success) {
|
|
15849
16288
|
child.masterOptions = apiResponse.data;
|
|
@@ -15930,11 +16369,11 @@ class FormComponent {
|
|
|
15930
16369
|
processForm() {
|
|
15931
16370
|
}
|
|
15932
16371
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FormComponent, deps: [{ token: MessageService }, { token: DataServiceLib }, { token: AuthService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15933
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: FormComponent, isStandalone: false, selector: "spa-form", inputs: { files: "files", data: "data", config: "config" }, outputs: { buttonClick: "buttonClick", inputChange: "inputChange" }, queries: [{ propertyName: "dynamicSelectTemplate", first: true, predicate: ["dynamicSelect"], descendants: true }], viewQueries: [{ propertyName: "defaultDynamicSelectTemplate", first: true, predicate: ["defaultDynamicSelect"], descendants: true, static: true }], ngImport: i0, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextMaskComponent, selector: "spa-text-mask", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "regex", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextAreaComponent, selector: "spa-text-area", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "rows", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextSingleComponent, selector: "spa-text-single", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction", "required", "min", "max", "regex", "suffix", "infoMessage", "field", "data", "detailsConfig", "masterField"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: LabelComponent, selector: "spa-label", inputs: ["display", "value", "format", "suffix", "size"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: MoneyComponent, selector: "spa-money", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "currency", "required", "min", "max", "infoMessage", "copyContent", "clearContent", "suffix"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: AttachComponent, selector: "spa-attach", inputs: ["fileOptions", "message", "files", "enableUpload"], outputs: ["filesChange", "upload"] }, { kind: "component", type: NumberComponent, selector: "spa-number", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "step", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: ViewerComponent, selector: "spa-viewer", inputs: ["fileAction", "path", "folderName", "fileNames", "removable", "display", "title"], outputs: ["remove"] }, { kind: "component", type: EmailComponent, selector: "spa-email", inputs: ["display", "value", "readonly", "required", "hint", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionValue"], outputs: ["valueChange"] }, { kind: "component", type: TextMultiComponent, selector: "spa-text-multi", inputs: ["display", "value", "readonly", "required", "hint", "strict", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: SelectMultiComponent, selector: "spa-select-multi", inputs: ["display", "value", "readonly", "required", "hint", "options", "optionDisplay", "optionValue", "infoMessage", "copyContent", "clearContent", "nullable", "placeholder", "width", "suffix", "loadAction", "selectAll"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: HtmlComponent, selector: "spa-html", inputs: ["value", "maxHeight", "display"] }, { kind: "component", type: EditorComponent, selector: "spa-editor", inputs: ["display", "value", "readonly", "required", "hint", "infoMessage", "placeholder", "width", "height", "minHeight", "defaultFontName", "editorConfig"], outputs: ["valueChange"] }, { kind: "component", type: NotesComponent, selector: "spa-notes", inputs: ["title", "notes", "loadAction", "loadIDField", "data", "nameField", "dateField", "commentField"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
16372
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: FormComponent, isStandalone: false, selector: "spa-form", inputs: { files: "files", data: "data", config: "config" }, outputs: { buttonClick: "buttonClick", inputChange: "inputChange" }, queries: [{ propertyName: "dynamicSelectTemplate", first: true, predicate: ["dynamicSelect"], descendants: true }], viewQueries: [{ propertyName: "defaultDynamicSelectTemplate", first: true, predicate: ["defaultDynamicSelect"], descendants: true, static: true }], ngImport: i0, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction(field)\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: TextComponent, selector: "spa-text", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "format", "type", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextMaskComponent, selector: "spa-text-mask", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "regex", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextAreaComponent, selector: "spa-text-area", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "rows", "width", "copyContent", "clearContent", "required", "min", "max", "regex", "suffix", "infoMessage"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: TextSingleComponent, selector: "spa-text-single", inputs: ["appearance", "readonly", "hint", "display", "placeholder", "value", "width", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction", "required", "min", "max", "regex", "suffix", "infoMessage", "field", "data", "detailsConfig", "masterField"], outputs: ["valueChange", "leave", "enterPress"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: DateComponent, selector: "spa-date", inputs: ["required", "min", "max", "readonly", "hint", "value", "display", "placeholder", "width", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: DatetimeComponent, selector: "spa-datetime", inputs: ["display", "value", "readonly", "width", "min", "max", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange"] }, { kind: "component", type: LabelComponent, selector: "spa-label", inputs: ["display", "value", "format", "suffix", "size"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: MoneyComponent, selector: "spa-money", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "currency", "required", "min", "max", "infoMessage", "copyContent", "clearContent", "suffix"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: AttachComponent, selector: "spa-attach", inputs: ["fileOptions", "message", "files", "enableUpload"], outputs: ["filesChange", "upload"] }, { kind: "component", type: NumberComponent, selector: "spa-number", inputs: ["readonly", "hint", "display", "placeholder", "value", "width", "required", "min", "max", "step", "suffix", "infoMessage", "copyContent", "clearContent"], outputs: ["valueChange", "leave", "enterPress", "infoClick"] }, { kind: "component", type: ViewerComponent, selector: "spa-viewer", inputs: ["fileAction", "path", "folderName", "fileNames", "removable", "display", "title"], outputs: ["remove"] }, { kind: "component", type: EmailComponent, selector: "spa-email", inputs: ["display", "value", "readonly", "required", "hint", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionValue"], outputs: ["valueChange"] }, { kind: "component", type: TextMultiComponent, selector: "spa-text-multi", inputs: ["display", "value", "readonly", "required", "hint", "strict", "suffix", "infoMessage", "copyContent", "clearContent", "options", "optionDisplay", "optionValue", "loadAction"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: SelectMultiComponent, selector: "spa-select-multi", inputs: ["display", "value", "readonly", "required", "hint", "options", "optionDisplay", "optionValue", "infoMessage", "copyContent", "clearContent", "nullable", "placeholder", "width", "suffix", "loadAction", "selectAll"], outputs: ["valueChange", "hoverChange"] }, { kind: "component", type: HtmlComponent, selector: "spa-html", inputs: ["value", "maxHeight", "display"] }, { kind: "component", type: EditorComponent, selector: "spa-editor", inputs: ["display", "value", "readonly", "required", "hint", "infoMessage", "placeholder", "width", "height", "minHeight", "defaultFontName", "editorConfig"], outputs: ["valueChange"] }, { kind: "component", type: NotesComponent, selector: "spa-notes", inputs: ["title", "notes", "loadAction", "loadIDField", "data", "nameField", "dateField", "commentField"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
15934
16373
|
}
|
|
15935
16374
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FormComponent, decorators: [{
|
|
15936
16375
|
type: Component,
|
|
15937
|
-
args: [{ selector: 'spa-form', standalone: false, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\n"] }]
|
|
16376
|
+
args: [{ selector: 'spa-form', standalone: false, template: "\n\n\n<div class=\"tin-form-container\" >\n <div [ngClass]=\"[multiColumn ? 'tin-grid' : 'tin-col', config.notesConfig ? 'width-75' : 'width-100']\" class=\"form-main-content\">\n\n <div *ngIf=\"!hasAccess\" class=\"tin-center\">\n <p><em>Access Restricted</em></p>\n </div>\n \n <div [ngClass]=\"field.span || field.type =='section' || field.type =='file' || field.type =='file-view' || field.type =='editor' ? 'span-col' : ''\" *ngFor=\"let field of visibleFields\"><!-- TS-11: bind cached visibleFields instead of getVisibleFields() per CD -->\n \n <ng-container>\n \n <ng-container [ngSwitch]=\"field.type\" class=\"highlight\">\n \n <div *ngSwitchCase=\"'section'\" class=\"title d-flex align-items-center\" (click)=\"toggleSection(field)\" style=\"cursor: pointer;\">\n <label style=\"font-size: larger;margin-right: 10px;\">{{field.alias ?? field.name | camelToWords}}</label>\n <mat-icon *ngIf=\"field.infoMessage\" (click)=\"onInfoClick($event, field.infoMessage)\" style=\"color: steelblue; font-size: 14px;\">info</mat-icon>\n <!-- <button mat-icon-button class=\"info-icon-button\" matTooltip=\"Info\" matTooltipPosition=\"above\">\n \n </button> -->\n <mat-icon *ngIf=\"hasSectionFields(field.name)\">{{shouldSectionCollapse(field) ? 'expand_more' : 'expand_less'}}</mat-icon>\n </div>\n \n <ng-container *ngSwitchCase=\"'file'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode !='view'\">\n <spa-attach [message]=\"field.alias ?? 'Drag and Drop files here'\" [(files)]=\"files\" [fileOptions]=\"field.fileOptions\"></spa-attach>\n </div>\n </ng-container>\n \n <ng-container *ngSwitchCase=\"'file-view'\">\n <div class=\"mt-1 mb-2\" *ngIf=\"config.mode && config.mode !='create'\">\n <spa-viewer [fileAction]=\"field.loadAction\" [path]=\"field.path\" [folderName]=\"data[field.keyField]\" ></spa-viewer>\n </div>\n </ng-container>\n \n <spa-html *ngSwitchCase=\"'html'\" [display]=\"field.alias | camelToWords\" [value]=\"data[field.name]\" [maxHeight]=\"field.maxHeight\"></spa-html>\n \n <label *ngSwitchCase=\"'blank'\"></label>\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-label *ngSwitchCase=\"'label'\" [display]=\"field.alias ?? field.name | camelToWords\" [value]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [format]=\"field.format ?? 'text'\" [suffix]=\"field.suffix\" [size]=\"field.size\"></spa-label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [infoMessage]=\"field.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [min]=\"field?.min\" [max]=\"field?.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [readonly]=\"testReadOnly(field)\" [min]=\"field.min\" [max]=\"field.max\" [infoMessage]=\"field.infoMessage\" ></spa-datetime>\n \n <spa-email *ngSwitchCase=\"'email'\" [display]=\"field.alias ?? field.name | camelToWords\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-email>\n \n <spa-text-mask *ngSwitchCase=\"'text-mask'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\"></spa-text-mask>\n \n \n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: field,\n field: field,\n data: data,\n testReadOnly: testReadOnly.bind(this),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n \n <spa-select-multi *ngSwitchCase=\"'select-multi'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field, data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [selectAll]=\"field.selectAll\">\n </spa-select-multi>\n \n <spa-text-multi *ngSwitchCase=\"'text-multi'\" [strict]=\"field.strict\" [display]=\"field.alias ?? field.name | camelToWords\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\"></spa-text-multi>\n \n \n \n <ng-container *ngSwitchCase=\"'composite'\">\n <div class=\"composite-field-container\">\n <div class=\"composite-field-group\">\n <ng-container *ngFor=\"let subfield of getVisibleSubfields(field)\">\n <ng-container [ngSwitch]=\"subfield.type\">\n \n <label *ngSwitchCase=\"'string'\" [ngStyle]=\"{'font-size':field.size ?? '14px'}\" >{{data[field.name] ?? field.alias ?? field.name}} {{field.suffix ?? ''}}</label>\n \n <spa-number *ngSwitchCase=\"'number'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\"></spa-number>\n \n <spa-money *ngSwitchCase=\"'money'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\"></spa-money>\n \n <spa-check *ngSwitchCase=\"'checkbox'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [infoMessage]=\"subfield.infoMessage\" ></spa-check>\n \n <spa-date *ngSwitchCase=\"'date'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" ></spa-date>\n \n <spa-datetime *ngSwitchCase=\"'datetime'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [infoMessage]=\"subfield.infoMessage\" ></spa-datetime>\n \n <!-- Fixed: composite select subfields projected the PARENT composite field into the\n dynamic select template (wrong options/value binding) and omitted testRequired,\n so the template's testRequired(field) call threw and killed the whole dialog.\n Context now mirrors the top-level select outlet, with the SUBFIELD, and readonly\n cascades composite parent || subfield like every other subfield type. -->\n <ng-container *ngSwitchCase=\"'select'\">\n <ng-container *ngTemplateOutlet=\"selectTemplate; context: {\n $implicit: subfield,\n field: subfield,\n data: data,\n testReadOnly: compositeReadOnly(field),\n testRequired: testRequired.bind(this),\n selectChanged: selectChanged.bind(this),\n resolveLoadAction: resolveLoadAction.bind(this)\n }\">\n </ng-container>\n </ng-container>\n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [options]=\"subfield.options\" [optionDisplay]=\"subfield.optionDisplay ?? 'name'\" [optionValue]=\"subfield.optionValue ?? 'value'\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [loadAction]=\"resolveLoadAction(subfield)\" [regex]=\"subfield.regex\" [field]=\"subfield\" [data]=\"data\" [detailsConfig]=\"subfield.detailsConfig\" [masterField]=\"subfield.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [rows]=\"subfield.rows\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [editorConfig]=\"subfield.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"subfield.alias ?? subfield.name | camelToWords\" [width]=\"subfield.width\" [(value)]=\"data[subfield.name]\" (valueChange)=\"inputChanged(subfield, $event)\" [required]=\"testRequired(subfield)\" [min]=\"subfield.min\" [max]=\"subfield.max\" [readonly]=\"testReadOnly(field) || testReadOnly(subfield)\" [hint]=\"subfield.hint\" [infoMessage]=\"subfield.infoMessage\" [suffix]=\"subfield.suffix\" [copyContent]=\"subfield.copyContent\" [clearContent]=\"subfield.clearContent\" [regex]=\"subfield.regex\"></spa-text>\n \n \n </ng-container>\n </ng-container>\n </div>\n </div>\n </ng-container>\n \n \n <spa-text-single *ngSwitchCase=\"'text-single'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [options]=\"field.options\" [optionDisplay]=\"field.optionDisplay ?? 'name'\" [optionValue]=\"field.optionValue ?? 'value'\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [loadAction]=\"resolveLoadAction(field)\" [regex]=\"field.regex\" [field]=\"field\" [data]=\"data\" [detailsConfig]=\"field.detailsConfig\" [masterField]=\"field.masterField\"></spa-text-single>\n\n <spa-text-area *ngSwitchCase=\"'text-area'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [rows]=\"field.rows\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text-area>\n\n <spa-editor *ngSwitchCase=\"'editor'\" [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [editorConfig]=\"field.editorConfig\"></spa-editor>\n\n <spa-text *ngSwitchDefault [display]=\"field.alias ?? field.name | camelToWords\" [width]=\"field.width\" [(value)]=\"data[field.name]\" (valueChange)=\"inputChanged(field,data[field.name])\" [required]=\"testRequired(field)\" [min]=\"field.min\" [max]=\"field.max\" [readonly]=\"testReadOnly(field)\" [hint]=\"field.hint\" [infoMessage]=\"field.infoMessage\" [suffix]=\"field.suffix\" [copyContent]=\"field.copyContent\" [clearContent]=\"field.clearContent\" [regex]=\"field.regex\"></spa-text>\n \n </ng-container>\n \n </ng-container>\n \n </div>\n \n \n <div class=\"span-col-center\" *ngIf=\"config.button\">\n <button mat-raised-button color=\"primary\" (click)=\"buttonClicked()\" cdkFocusInitial>{{buttonDisplay}}</button>\n </div>\n \n \n </div>\n <!-- Fixed: built-in fallback select template \u2014 spa-form used consumers' projected #dynamicSelect\n (only detailsDialog provides one), so selects silently rendered NOTHING when spa-form was used\n directly. The projected template (if any) still wins; this default carries the same bindings. -->\n <ng-template #defaultDynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction(field)\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n\n <div class=\"notes-section\" *ngIf=\"config.notesConfig\">\n <spa-notes\n [title]=\"config.notesConfig.title || 'Notes'\"\n [notes]=\"config.notesConfig.notes || []\"\n [loadAction]=\"config.notesConfig.loadAction\"\n [loadIDField]=\"config.notesConfig.loadIDField\"\n [data]=\"data\"\n [nameField]=\"config.notesConfig.nameField || 'createdByName'\"\n [dateField]=\"config.notesConfig.dateField || 'createdDate'\"\n [commentField]=\"config.notesConfig.commentField || 'details'\">\n </spa-notes>\n </div>\n</div>\n\n", styles: [".title{margin-top:.5em;margin-bottom:.5em;font-size:larger;font-weight:300;color:#0b447e}.composite-field-group{display:flex;flex-direction:row;flex-wrap:wrap;gap:12px}.tin-form-container{display:flex;flex-direction:row;width:100%;gap:16px}.width-100{width:100%!important}.width-75{width:70%!important}.notes-section{width:400px;border-left:1px solid #e0e0e0}\n"] }]
|
|
15938
16377
|
}], ctorParameters: () => [{ type: MessageService }, { type: DataServiceLib }, { type: AuthService }], propDecorators: { dynamicSelectTemplate: [{
|
|
15939
16378
|
type: ContentChild,
|
|
15940
16379
|
args: ['dynamicSelect']
|
|
@@ -15957,6 +16396,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
15957
16396
|
class TabsComponent {
|
|
15958
16397
|
constructor(tabService) {
|
|
15959
16398
|
this.tabService = tabService;
|
|
16399
|
+
this.countsInitialized = false; // Added: guards refreshResolvedCounts until ngOnInit has seeded tabCounts/countUrls
|
|
16400
|
+
this.countUrls = {}; // Added: last resolved countAction URL per tab
|
|
15960
16401
|
this.tableConfigs = [];
|
|
15961
16402
|
this.localMode = false; // Added: one-step create — tables run against in-memory rows on parentDetails
|
|
15962
16403
|
this.nestingLevel = 0; // Changed: Track nesting depth for dialog recursion control
|
|
@@ -15973,6 +16414,8 @@ class TabsComponent {
|
|
|
15973
16414
|
const initialized = this.tabService.initializeTabs(this.tableConfigs, this.parentDetails);
|
|
15974
16415
|
this.tabCounts = initialized.tabCounts;
|
|
15975
16416
|
this.tableReloads = initialized.tableReloads;
|
|
16417
|
+
this.seedCountUrls(); // Added: snapshot the URL each badge was fetched with so we only re-fetch when the parent id actually changes
|
|
16418
|
+
this.countsInitialized = true; // Added: ngOnChanges runs BEFORE ngOnInit on the first bind — counts are seeded here, never there
|
|
15976
16419
|
this.updateVisibleTabs(); // Changed: Cache visible tabs on init
|
|
15977
16420
|
// Added: targeted sibling refresh — the host pushes a tab index (into tableConfigs) to reload that tab's
|
|
15978
16421
|
// table and count badge (e.g. a custom catalog tab submitting a request refreshes the My Requests tab).
|
|
@@ -15985,7 +16428,33 @@ class TabsComponent {
|
|
|
15985
16428
|
ngOnChanges(changes) {
|
|
15986
16429
|
if (changes['tableConfigs'] || changes['parentDetails']) {
|
|
15987
16430
|
this.updateVisibleTabs();
|
|
15988
|
-
|
|
16431
|
+
// Added: details dialogs load the parent record asynchronously, so the counts fired at init had an unresolved
|
|
16432
|
+
// {id} placeholder — re-fire them now that the id is known, otherwise the badge keeps the global count forever
|
|
16433
|
+
if (this.countsInitialized)
|
|
16434
|
+
this.refreshResolvedCounts();
|
|
16435
|
+
}
|
|
16436
|
+
}
|
|
16437
|
+
// Added: record the resolved count URL per tab (still holds {id} when the parent record has not loaded yet)
|
|
16438
|
+
seedCountUrls() {
|
|
16439
|
+
this.countUrls = {};
|
|
16440
|
+
(this.tableConfigs ?? []).forEach((config, index) => { this.countUrls[index] = this.tabService.resolveCountUrl(config, this.parentDetails); });
|
|
16441
|
+
}
|
|
16442
|
+
// Added: re-fetch only badges whose resolved URL changed — keeps auto-refresh ticks from re-firing every count
|
|
16443
|
+
refreshResolvedCounts() {
|
|
16444
|
+
(this.tableConfigs ?? []).forEach((config, index) => {
|
|
16445
|
+
const url = this.tabService.resolveCountUrl(config, this.parentDetails);
|
|
16446
|
+
if (!url || url === this.countUrls[index])
|
|
16447
|
+
return;
|
|
16448
|
+
this.countUrls[index] = url;
|
|
16449
|
+
this.refreshTabCount(index);
|
|
16450
|
+
});
|
|
16451
|
+
}
|
|
16452
|
+
// Added: the tab's own grid is the authority on how many rows belong to this parent, so adopt its total —
|
|
16453
|
+
// the badge then always matches the "1 – 1 of 1" shown under the tabs and cannot drift from a separate count call
|
|
16454
|
+
onTabTotal(index, total) {
|
|
16455
|
+
if (total === null || total === undefined)
|
|
16456
|
+
return;
|
|
16457
|
+
this.tabCounts[index] = total;
|
|
15989
16458
|
}
|
|
15990
16459
|
// Changed: Use service for tab change handling
|
|
15991
16460
|
onTabChange(event) {
|
|
@@ -16036,11 +16505,11 @@ class TabsComponent {
|
|
|
16036
16505
|
return this.reload || this.tableReloads[index];
|
|
16037
16506
|
}
|
|
16038
16507
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TabsComponent, deps: [{ token: TabService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16039
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TabsComponent, isStandalone: false, selector: "spa-tabs", inputs: { tableConfigs: "tableConfigs", reload: "reload", reloadTab: "reloadTab", parentDetails: "parentDetails", localMode: "localMode", nestingLevel: "nestingLevel" }, outputs: { formRefresh: "formRefresh", actionSuccess: "actionSuccess" }, usesOnChanges: true, ngImport: i0, template: "<mat-tab-group (selectedTabChange)=\"onTabChange($event)\" [selectedIndex]=\"selectedTabIndex\">\n\n <!-- Changed: Use cached visibleTabs property to prevent infinite change detection loop -->\n <ng-container *ngFor=\"let tab of visibleTabs; let i = index\">\n <mat-tab><!-- TS-11: visibleTabs is already filtered via getVisibleTabs; dropped per-tab isTabVisible() call per CD -->\n\n <!-- Tab label with optional count badge -->\n <ng-template matTabLabel>\n <span\n [matBadge]=\"shouldShowBadge(tab.originalIndex) ? getTabCount(tab.originalIndex) : null\"\n [matBadgeHidden]=\"!shouldShowBadge(tab.originalIndex)\"\n matBadgeOverlap=\"false\">\n {{getTabTitle(tab.config)}}\n </span>\n </ng-template>\n\n <!-- Tab content: custom template when provided (tabTemplate), otherwise the lazy-loaded table.\n Templates keep the same lazy semantics \u2014 rendered only once the tab has been activated. -->\n <div *ngIf=\"shouldLoadTabData(i) && tab.config.tabTemplate\" class=\"tab-content\">\n <ng-container [ngTemplateOutlet]=\"tab.config.tabTemplate\"></ng-container>\n </div>\n\n <div *ngIf=\"shouldLoadTabData(i) && !tab.config.tabTemplate\" class=\"tab-content\">\n <spa-table\n [config]=\"tab.config\"\n [reload]=\"getReloadSubject(tab.originalIndex)\"\n [inTab]=\"true\"\n [activeTab]=\"selectedTabIndex === i\"\n [nestingLevel]=\"nestingLevel\"\n [localMode]=\"localMode\"\n [parentDetails]=\"parentDetails\"\n (actionSuccess)=\"onTableActionSuccess(tab.originalIndex, $event)\">\n </spa-table><!-- Changed: localMode + parentDetails passed through for one-step create local tables -->\n </div>\n\n <!-- Placeholder for non-loaded tabs -->\n <div *ngIf=\"!shouldLoadTabData(i)\" class=\"tab-placeholder\">\n <!-- Empty placeholder - content will load when tab is activated -->\n </div>\n\n </mat-tab>\n </ng-container>\n\n</mat-tab-group>", styles: [":host{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-group{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-body-wrapper{flex:1}.tab-content{padding-top:16px;overflow-x:auto}.tab-placeholder{min-height:100px;display:flex;align-items:center;justify-content:center}.badge{background-color:#2196f3;color:#fff;border-radius:12px;padding:2px 8px;margin-left:8px;font-size:12px}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i4$2.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "directive", type: i5$2.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: i5$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
16508
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TabsComponent, isStandalone: false, selector: "spa-tabs", inputs: { tableConfigs: "tableConfigs", reload: "reload", reloadTab: "reloadTab", parentDetails: "parentDetails", localMode: "localMode", nestingLevel: "nestingLevel" }, outputs: { formRefresh: "formRefresh", actionSuccess: "actionSuccess" }, usesOnChanges: true, ngImport: i0, template: "<mat-tab-group (selectedTabChange)=\"onTabChange($event)\" [selectedIndex]=\"selectedTabIndex\">\n\n <!-- Changed: Use cached visibleTabs property to prevent infinite change detection loop -->\n <ng-container *ngFor=\"let tab of visibleTabs; let i = index\">\n <mat-tab><!-- TS-11: visibleTabs is already filtered via getVisibleTabs; dropped per-tab isTabVisible() call per CD -->\n\n <!-- Tab label with optional count badge -->\n <ng-template matTabLabel>\n <span\n [matBadge]=\"shouldShowBadge(tab.originalIndex) ? getTabCount(tab.originalIndex) : null\"\n [matBadgeHidden]=\"!shouldShowBadge(tab.originalIndex)\"\n matBadgeOverlap=\"false\">\n {{getTabTitle(tab.config)}}\n </span>\n </ng-template>\n\n <!-- Tab content: custom template when provided (tabTemplate), otherwise the lazy-loaded table.\n Templates keep the same lazy semantics \u2014 rendered only once the tab has been activated. -->\n <div *ngIf=\"shouldLoadTabData(i) && tab.config.tabTemplate\" class=\"tab-content\">\n <ng-container [ngTemplateOutlet]=\"tab.config.tabTemplate\"></ng-container>\n </div>\n\n <div *ngIf=\"shouldLoadTabData(i) && !tab.config.tabTemplate\" class=\"tab-content\">\n <spa-table\n [config]=\"tab.config\"\n [reload]=\"getReloadSubject(tab.originalIndex)\"\n [inTab]=\"true\"\n [activeTab]=\"selectedTabIndex === i\"\n [nestingLevel]=\"nestingLevel\"\n [localMode]=\"localMode\"\n [parentDetails]=\"parentDetails\"\n (totalChange)=\"onTabTotal(tab.originalIndex, $event)\"\n (actionSuccess)=\"onTableActionSuccess(tab.originalIndex, $event)\">\n </spa-table><!-- Changed: badge adopts the grid's filtered total so it can never show a global/unfiltered count --><!-- Changed: localMode + parentDetails passed through for one-step create local tables -->\n </div>\n\n <!-- Placeholder for non-loaded tabs -->\n <div *ngIf=\"!shouldLoadTabData(i)\" class=\"tab-placeholder\">\n <!-- Empty placeholder - content will load when tab is activated -->\n </div>\n\n </mat-tab>\n </ng-container>\n\n</mat-tab-group>", styles: [":host{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-group{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-body-wrapper{flex:1}.tab-content{padding-top:16px;overflow-x:auto}.tab-placeholder{min-height:100px;display:flex;align-items:center;justify-content:center}.badge{background-color:#2196f3;color:#fff;border-radius:12px;padding:2px 8px;margin-left:8px;font-size:12px}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i4$2.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "directive", type: i5$2.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: i5$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
16040
16509
|
}
|
|
16041
16510
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TabsComponent, decorators: [{
|
|
16042
16511
|
type: Component,
|
|
16043
|
-
args: [{ selector: 'spa-tabs', standalone: false, template: "<mat-tab-group (selectedTabChange)=\"onTabChange($event)\" [selectedIndex]=\"selectedTabIndex\">\n\n <!-- Changed: Use cached visibleTabs property to prevent infinite change detection loop -->\n <ng-container *ngFor=\"let tab of visibleTabs; let i = index\">\n <mat-tab><!-- TS-11: visibleTabs is already filtered via getVisibleTabs; dropped per-tab isTabVisible() call per CD -->\n\n <!-- Tab label with optional count badge -->\n <ng-template matTabLabel>\n <span\n [matBadge]=\"shouldShowBadge(tab.originalIndex) ? getTabCount(tab.originalIndex) : null\"\n [matBadgeHidden]=\"!shouldShowBadge(tab.originalIndex)\"\n matBadgeOverlap=\"false\">\n {{getTabTitle(tab.config)}}\n </span>\n </ng-template>\n\n <!-- Tab content: custom template when provided (tabTemplate), otherwise the lazy-loaded table.\n Templates keep the same lazy semantics \u2014 rendered only once the tab has been activated. -->\n <div *ngIf=\"shouldLoadTabData(i) && tab.config.tabTemplate\" class=\"tab-content\">\n <ng-container [ngTemplateOutlet]=\"tab.config.tabTemplate\"></ng-container>\n </div>\n\n <div *ngIf=\"shouldLoadTabData(i) && !tab.config.tabTemplate\" class=\"tab-content\">\n <spa-table\n [config]=\"tab.config\"\n [reload]=\"getReloadSubject(tab.originalIndex)\"\n [inTab]=\"true\"\n [activeTab]=\"selectedTabIndex === i\"\n [nestingLevel]=\"nestingLevel\"\n [localMode]=\"localMode\"\n [parentDetails]=\"parentDetails\"\n (actionSuccess)=\"onTableActionSuccess(tab.originalIndex, $event)\">\n </spa-table><!-- Changed: localMode + parentDetails passed through for one-step create local tables -->\n </div>\n\n <!-- Placeholder for non-loaded tabs -->\n <div *ngIf=\"!shouldLoadTabData(i)\" class=\"tab-placeholder\">\n <!-- Empty placeholder - content will load when tab is activated -->\n </div>\n\n </mat-tab>\n </ng-container>\n\n</mat-tab-group>", styles: [":host{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-group{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-body-wrapper{flex:1}.tab-content{padding-top:16px;overflow-x:auto}.tab-placeholder{min-height:100px;display:flex;align-items:center;justify-content:center}.badge{background-color:#2196f3;color:#fff;border-radius:12px;padding:2px 8px;margin-left:8px;font-size:12px}\n"] }]
|
|
16512
|
+
args: [{ selector: 'spa-tabs', standalone: false, template: "<mat-tab-group (selectedTabChange)=\"onTabChange($event)\" [selectedIndex]=\"selectedTabIndex\">\n\n <!-- Changed: Use cached visibleTabs property to prevent infinite change detection loop -->\n <ng-container *ngFor=\"let tab of visibleTabs; let i = index\">\n <mat-tab><!-- TS-11: visibleTabs is already filtered via getVisibleTabs; dropped per-tab isTabVisible() call per CD -->\n\n <!-- Tab label with optional count badge -->\n <ng-template matTabLabel>\n <span\n [matBadge]=\"shouldShowBadge(tab.originalIndex) ? getTabCount(tab.originalIndex) : null\"\n [matBadgeHidden]=\"!shouldShowBadge(tab.originalIndex)\"\n matBadgeOverlap=\"false\">\n {{getTabTitle(tab.config)}}\n </span>\n </ng-template>\n\n <!-- Tab content: custom template when provided (tabTemplate), otherwise the lazy-loaded table.\n Templates keep the same lazy semantics \u2014 rendered only once the tab has been activated. -->\n <div *ngIf=\"shouldLoadTabData(i) && tab.config.tabTemplate\" class=\"tab-content\">\n <ng-container [ngTemplateOutlet]=\"tab.config.tabTemplate\"></ng-container>\n </div>\n\n <div *ngIf=\"shouldLoadTabData(i) && !tab.config.tabTemplate\" class=\"tab-content\">\n <spa-table\n [config]=\"tab.config\"\n [reload]=\"getReloadSubject(tab.originalIndex)\"\n [inTab]=\"true\"\n [activeTab]=\"selectedTabIndex === i\"\n [nestingLevel]=\"nestingLevel\"\n [localMode]=\"localMode\"\n [parentDetails]=\"parentDetails\"\n (totalChange)=\"onTabTotal(tab.originalIndex, $event)\"\n (actionSuccess)=\"onTableActionSuccess(tab.originalIndex, $event)\">\n </spa-table><!-- Changed: badge adopts the grid's filtered total so it can never show a global/unfiltered count --><!-- Changed: localMode + parentDetails passed through for one-step create local tables -->\n </div>\n\n <!-- Placeholder for non-loaded tabs -->\n <div *ngIf=\"!shouldLoadTabData(i)\" class=\"tab-placeholder\">\n <!-- Empty placeholder - content will load when tab is activated -->\n </div>\n\n </mat-tab>\n </ng-container>\n\n</mat-tab-group>", styles: [":host{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-group{flex:1;display:flex;flex-direction:column}:host ::ng-deep .mat-mdc-tab-body-wrapper{flex:1}.tab-content{padding-top:16px;overflow-x:auto}.tab-placeholder{min-height:100px;display:flex;align-items:center;justify-content:center}.badge{background-color:#2196f3;color:#fff;border-radius:12px;padding:2px 8px;margin-left:8px;font-size:12px}\n"] }]
|
|
16044
16513
|
}], ctorParameters: () => [{ type: TabService }], propDecorators: { tableConfigs: [{
|
|
16045
16514
|
type: Input
|
|
16046
16515
|
}], reload: [{
|
|
@@ -16591,11 +17060,11 @@ class DetailsDialog {
|
|
|
16591
17060
|
}
|
|
16592
17061
|
}
|
|
16593
17062
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DetailsDialog, deps: [{ token: i1$3.BreakpointObserver }, { token: LoaderService }, { token: DataServiceLib }, { token: MessageService }, { token: i4.MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: ButtonService }, { token: DialogService }, { token: AuthService }, { token: TableConfigService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16594
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DetailsDialog, isStandalone: false, selector: "spa-detailsDialog", outputs: { inputChange: "inputChange" }, ngImport: i0, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <mat-progress-bar mode=\"indeterminate\" *ngIf=\"isProcessing && dataService.appConfig.progressLine\"></mat-progress-bar>\n\n <div class=\"d-flex justify-content-between align-items-center mt-2\" style=\"padding-left: 24px; padding-right: 24px;\">\n\n <div>\n <label style=\"font-size: 20px; font-weight:500;margin-top: 10px;margin-bottom: 5px;\" >{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <button [disabled]=\"isProcessing\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <p *ngIf=\"formConfig && !details\"><em>Loading...</em></p>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: i7$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i14.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: StepsComponent, selector: "spa-steps", inputs: ["value", "config", "data"] }, { kind: "component", type: FormComponent, selector: "spa-form", inputs: ["files", "data", "config"], outputs: ["buttonClick", "inputChange"] }, { kind: "component", type: AlertComponent, selector: "spa-alert", inputs: ["config", "data"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }, { kind: "component", type: StatusesComponent, selector: "spa-statuses", inputs: ["config", "data"] }, { kind: "pipe", type: i1.TitleCasePipe, name: "titlecase" }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
17063
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: DetailsDialog, isStandalone: false, selector: "spa-detailsDialog", outputs: { inputChange: "inputChange" }, ngImport: i0, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <mat-progress-bar mode=\"indeterminate\" *ngIf=\"isProcessing && dataService.appConfig.progressLine\"></mat-progress-bar>\n\n <div class=\"d-flex justify-content-between align-items-center mt-2\" style=\"padding-left: 24px; padding-right: 24px;\">\n\n <div>\n <label style=\"font-size: 20px; font-weight:500;margin-top: 10px;margin-bottom: 5px;\" >{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <button [disabled]=\"isProcessing\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <p *ngIf=\"formConfig && !details\"><em>Loading...</em></p>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction ? resolveLoadAction(field) : field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: i7$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i14.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: StepsComponent, selector: "spa-steps", inputs: ["value", "config", "data"] }, { kind: "component", type: FormComponent, selector: "spa-form", inputs: ["files", "data", "config"], outputs: ["buttonClick", "inputChange"] }, { kind: "component", type: AlertComponent, selector: "spa-alert", inputs: ["config", "data"] }, { kind: "component", type: TabsComponent, selector: "spa-tabs", inputs: ["tableConfigs", "reload", "reloadTab", "parentDetails", "localMode", "nestingLevel"], outputs: ["formRefresh", "actionSuccess"] }, { kind: "component", type: StatusesComponent, selector: "spa-statuses", inputs: ["config", "data"] }, { kind: "pipe", type: i1.TitleCasePipe, name: "titlecase" }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }] }); }
|
|
16595
17064
|
}
|
|
16596
17065
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: DetailsDialog, decorators: [{
|
|
16597
17066
|
type: Component,
|
|
16598
|
-
args: [{ selector: 'spa-detailsDialog', standalone: false, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <mat-progress-bar mode=\"indeterminate\" *ngIf=\"isProcessing && dataService.appConfig.progressLine\"></mat-progress-bar>\n\n <div class=\"d-flex justify-content-between align-items-center mt-2\" style=\"padding-left: 24px; padding-right: 24px;\">\n\n <div>\n <label style=\"font-size: 20px; font-weight:500;margin-top: 10px;margin-bottom: 5px;\" >{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <button [disabled]=\"isProcessing\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <p *ngIf=\"formConfig && !details\"><em>Loading...</em></p>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}\n"] }]
|
|
17067
|
+
args: [{ selector: 'spa-detailsDialog', standalone: false, template: "<!-- Changed: dialog-has-tables mirrors the service's hasTables (tableConfigs?.length)\n which decides height '90%' vs 'auto' \u2014 used to scope scroll behaviour per case -->\n<div class=\"dialog-container\" [class.dialog-has-tables]=\"detailsConfig.tableConfigs?.length > 0\">\n\n <div class=\"dialog-content\">\n\n <mat-progress-bar mode=\"indeterminate\" *ngIf=\"isProcessing && dataService.appConfig.progressLine\"></mat-progress-bar>\n\n <div class=\"d-flex justify-content-between align-items-center mt-2\" style=\"padding-left: 24px; padding-right: 24px;\">\n\n <div>\n <label style=\"font-size: 20px; font-weight:500;margin-top: 10px;margin-bottom: 5px;\" >{{titleAction | titlecase}} {{formConfig?.title}}</label>\n </div>\n\n <div class=\"d-flex align-items-center\" style=\"gap: 8px;\">\n\n <!-- Changed: Auto Refresh icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.autoRefreshConfig\" mat-icon-button\n [matTooltip]=\"autoRefreshEnabled ? 'Click to disable auto refresh' : 'Click to enable auto refresh'\" matTooltipPosition=\"above\"\n [style.color]=\"autoRefreshEnabled ? '#4caf50' : '#9e9e9e'\"\n (click)=\"autoRefreshEnabled = !autoRefreshEnabled; toggleAutoRefresh()\">\n <mat-icon>autorenew</mat-icon>\n </button>\n <!-- Changed: Keep Open icon button \u2014 grey when off, green when on, with dynamic tooltip -->\n <button *ngIf=\"detailsConfig.allowUserKeepOpen\" mat-icon-button\n [matTooltip]=\"userKeepOpen ? 'Click to disable keep open' : 'Click to enable keep open'\" matTooltipPosition=\"above\"\n [style.color]=\"userKeepOpen ? '#4caf50' : '#9e9e9e'\"\n (click)=\"userKeepOpen = !userKeepOpen\">\n <mat-icon>push_pin</mat-icon>\n </button>\n\n <!-- TS-16: bind precomputed editButtonVM instead of testVisible/testDisabled (which cloned objects) per CD -->\n <div *ngIf=\"formConfig.mode=='view' && editButton && editButtonVM.visible\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Edit\" color=\"primary\" (click)=\"setMode('edit')\" [disabled]=\"editButtonVM.disabled\"><mat-icon>edit</mat-icon></button>\n </div>\n\n <button [disabled]=\"isProcessing\" *ngIf=\"loadByAction\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Refresh\" color=\"primary\" (click)=\"loadData(formConfig.loadAction, detailsConfig.causeTableRefresh)\"><mat-icon class=\"refreshIcon\">cached</mat-icon></button>\n \n <!-- Added: Top close button when position is 'top' -->\n <button *ngIf=\"shouldShowTopClose()\" [disabled]=\"isProcessing\" mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Close\" (click)=\"close()\"><mat-icon>close</mat-icon></button>\n\n </div>\n\n </div>\n\n <div style=\"padding-left: 24px; padding-right: 24px;\">\n <spa-steps *ngIf=\"stepConfig && details && stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n </div>\n\n <mat-dialog-content class=\"mat-typography dialog-scroll-content\">\n\n <spa-steps *ngIf=\"stepConfig && details && !stepConfig.sticky\" [config]=\"stepConfig\" [data]=\"details\"></spa-steps>\n <spa-statuses *ngIf=\"statusConfig && details && !statusConfig?.sticky\" [config]=\"statusConfig\" [data]=\"details\"></spa-statuses>\n <spa-alert *ngIf=\"formConfig.alertConfig && !formConfig.alertConfig?.sticky\" [config]=\"formConfig.alertConfig\" [data]=\"details\"></spa-alert>\n\n <div class=\"tin-input\" style=\"font-size:14px\">\n\n <p *ngIf=\"formConfig && !details\"><em>Loading...</em></p>\n\n <spa-form *ngIf=\"formConfig && details\" [files]=\"files\" [data]=\"details\" [config]=\"formConfig\" (inputChange)=\"inputChanged($event)\">\n <ng-template #dynamicSelect let-field=\"field\" let-data=\"data\" let-testReadOnly=\"testReadOnly\" let-testRequired=\"testRequired\" let-selectChanged=\"selectChanged\" let-resolveLoadAction=\"resolveLoadAction\">\n <spa-select\n [display]=\"field.alias ?? field.name | camelToWords\"\n [width]=\"field.width\"\n [nullable]=\"field.nullable\"\n [options]=\"field.options\"\n [masterOptions]=\"field.masterOptions\"\n [masterField]=\"field.masterField\"\n [optionDisplay]=\"field.optionDisplay ?? 'name'\"\n [optionValue]=\"field.optionValue ?? 'value'\"\n [(value)]=\"data[field.name]\"\n [defaultFirstValue]=\"field.defaultFirstValue\"\n [required]=\"testRequired(field)\"\n [readonly]=\"testReadOnly(field)\"\n [hint]=\"field.hint\"\n [detailsConfig]=\"field.detailsConfig\"\n [loadAction]=\"resolveLoadAction ? resolveLoadAction(field) : field.loadAction\"\n [loadIDField]=\"field.loadIDField\"\n [field]=\"field\"\n [data]=\"data\"\n [infoMessage]=\"field.infoMessage\"\n [copyContent]=\"field.copyContent\"\n (valueChange)=\"selectChanged(field)\"\n ></spa-select>\n </ng-template>\n </spa-form>\n\n <!-- Changed: Use unified spa-tabs with nestingLevel control \u2014 tabs hidden when nestingLevel >= 2 -->\n <spa-tabs\n *ngIf=\"showTabs && tableConfigs && !(detailsConfig.hideTablesInCreateMode && formConfig?.mode === 'create')\"\n [tableConfigs]=\"tableConfigs\"\n [reload]=\"tableReload\"\n [parentDetails]=\"details\"\n [localMode]=\"formConfig?.mode === 'create'\"\n [nestingLevel]=\"nestingLevel + 1\"\n (formRefresh)=\"loadData(formConfig.loadAction, false)\"\n (actionSuccess)=\"actionPerformed = true\">\n </spa-tabs><!-- Changed: nested-tab table actions flag actionPerformed so refreshOnCloseIfActioned refreshes the parent on close -->\n\n </div>\n\n </mat-dialog-content>\n\n\n </div>\n\n <mat-dialog-actions >\n\n <div>\n\n <button mat-raised-button [disabled]=\"isProcessing\" *ngIf=\"formConfig.mode=='create' && createButton\" color=\"primary\"\n (click)=\"create()\" cdkFocusInitial>{{createButton.display ?? 'Submit'}}\n </button>\n\n <button mat-raised-button [disabled]=\"isProcessing\" *ngIf=\"formConfig.mode=='edit' && editButton\" color=\"primary\"\n (click)=\"edit()\" cdkFocusInitial>{{editButton.display ?? 'Submit'}}\n </button>\n\n <!-- TS-16: bind precomputed extraButtonVMs instead of testVisible/testDisabled/getButtonColor (each cloning objects) per CD -->\n <ng-container *ngFor=\"let vm of extraButtonVMs\">\n <button *ngIf=\"formConfig.mode !== 'create' && vm.visible\" mat-stroked-button [disabled]=\"isProcessing || vm.disabled\" [ngStyle]=\"{'color': vm.color}\" (click)=\"custom(vm.button)\" cdkFocusInitial>\n <mat-icon *ngIf=\"vm.button.icon\" [ngStyle]=\"{'color': vm.color}\">{{vm.button.icon.name}}</mat-icon>\n {{vm.button.display ?? vm.button.name | titlecase}}\n </button>\n </ng-container>\n\n <!-- Changed: Bottom close button now uses conditional display and custom text -->\n <button *ngIf=\"shouldShowBottomClose()\" mat-stroked-button color=\"primary\" (click)=\"close()\">{{getCloseText()}}</button>\n\n </div>\n\n <div class=\"col d-flex justify-content-end\" *ngIf=\"smallScreen\">\n <button mat-icon-button matTooltipPosition=\"above\" matTooltip=\"Delete\" [disabled]=\"isProcessing\" style=\"color: red;\" (click)=\"delete()\" *ngIf=\"formConfig.mode!='create' && deleteButton\"><mat-icon>delete</mat-icon></button>\n </div>\n\n\n </mat-dialog-actions>\n\n\n</div>\n\n\n\n\n\n\n", styles: [".top{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-bottom:10px;margin-top:10px}.mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}.mat-icon-button{width:32px;height:32px}.mat-icon-button mat-icon{font-size:20px;margin-top:-7px}.col-icon{margin-left:10px}.title{margin-top:10px;font-size:larger;font-weight:300}.make-gray{background-color:#f5f5f5}tr.mat-mdc-row{transition:background-color .15s ease}tr.mat-mdc-row:hover{background-color:#2196f305}tr.mat-mdc-row.row-editing,tr.mat-mdc-row.row-editing:hover{background-color:#ffa00014}.inline-save{color:#2e7d32!important}.inline-cancel{color:#c62828!important}.right-padding{padding-right:10px}.action-buttons-container{display:flex;justify-content:flex-end;align-items:center}.refreshIcon{font-size:22px!important;margin-top:-7px!important}.dialog-container{display:flex;flex-direction:column;height:100%;max-height:100%;min-height:0}.dialog-content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.dialog-scroll-content{flex:1;overflow-y:auto;max-height:none!important;display:flex;flex-direction:column;min-height:0}.dialog-container:not(.dialog-has-tables){height:auto}.dialog-container:not(.dialog-has-tables) .dialog-scroll-content{flex:1 1 auto}.dialog-scroll-content>.tin-input{flex:1;display:flex;flex-direction:column}.dialog-scroll-content>.tin-input>spa-tabs{flex:1;display:flex;flex-direction:column}mat-dialog-actions{flex-shrink:0;justify-content:flex-start}.paginator-hidden{display:none!important}.paged-filter-hint{display:flex;align-items:center;gap:8px;margin:4px 0 8px;padding:6px 12px;border-radius:4px;background-color:#fff8e1;color:#795548;font-size:13px}.paged-filter-hint mat-icon{font-size:18px;width:18px;height:18px;color:#ffa000}\n"] }]
|
|
16599
17068
|
}], ctorParameters: () => [{ type: i1$3.BreakpointObserver }, { type: LoaderService }, { type: DataServiceLib }, { type: MessageService }, { type: i4.MatDialogRef }, { type: DetailsDialogConfig, decorators: [{
|
|
16600
17069
|
type: Inject,
|
|
16601
17070
|
args: [MAT_DIALOG_DATA]
|
|
@@ -16653,7 +17122,7 @@ class ListDialogComponent {
|
|
|
16653
17122
|
}
|
|
16654
17123
|
}
|
|
16655
17124
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ListDialogComponent, deps: [{ token: DataServiceLib }, { token: i4.MatDialogRef }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16656
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: ListDialogComponent, isStandalone: false, selector: "spa-list-dialog", ngImport: i0, template: "<div>\n\n <label style=\"font-size: 24px;\">{{listConfig?.title ?? 'List'}}</label>\n\n</div>\n\n<mat-dialog-content class=\"mat-typography\">\n\n <div style=\" font-size: 14px;\">\n <spa-table [config]=\"config\" [reload]=\"tableReload\" (actionClick)=\"actionClicked()\"></spa-table>\n </div>\n\n\n</mat-dialog-content>\n\n<mat-dialog-actions>\n <button mat-button (click)=\"close()\" >Ok</button>\n</mat-dialog-actions>\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"], dependencies: [{ kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
17125
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: ListDialogComponent, isStandalone: false, selector: "spa-list-dialog", ngImport: i0, template: "<div>\n\n <label style=\"font-size: 24px;\">{{listConfig?.title ?? 'List'}}</label>\n\n</div>\n\n<mat-dialog-content class=\"mat-typography\">\n\n <div style=\" font-size: 14px;\">\n <spa-table [config]=\"config\" [reload]=\"tableReload\" (actionClick)=\"actionClicked()\"></spa-table>\n </div>\n\n\n</mat-dialog-content>\n\n<mat-dialog-actions>\n <button mat-button (click)=\"close()\" >Ok</button>\n</mat-dialog-actions>\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"], dependencies: [{ kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i4.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i4.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
16657
17126
|
}
|
|
16658
17127
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ListDialogComponent, decorators: [{
|
|
16659
17128
|
type: Component,
|
|
@@ -16720,7 +17189,7 @@ class InvitationsTableComponent {
|
|
|
16720
17189
|
});
|
|
16721
17190
|
}
|
|
16722
17191
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InvitationsTableComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: AuthService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16723
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: InvitationsTableComponent, isStandalone: false, selector: "spa-invitations-table", ngImport: i0, template: "<spa-table [config]=\"invitationsTableConfig\" (actionClick)=\"invActionClicked($event)\" [reload]=\"tableReload\"></spa-table>\n", styles: [""], dependencies: [{ kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
17192
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: InvitationsTableComponent, isStandalone: false, selector: "spa-invitations-table", ngImport: i0, template: "<spa-table [config]=\"invitationsTableConfig\" (actionClick)=\"invActionClicked($event)\" [reload]=\"tableReload\"></spa-table>\n", styles: [""], dependencies: [{ kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
16724
17193
|
}
|
|
16725
17194
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: InvitationsTableComponent, decorators: [{
|
|
16726
17195
|
type: Component,
|
|
@@ -16898,7 +17367,7 @@ class PageComponent {
|
|
|
16898
17367
|
this.dataLoad.emit(x);
|
|
16899
17368
|
}
|
|
16900
17369
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PageComponent, deps: [{ token: DataServiceLib }, { token: MessageService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16901
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PageComponent, isStandalone: false, selector: "spa-page", inputs: { config: "config" }, outputs: { searchModeActivated: "searchModeActivated", searchModeDeactivated: "searchModeDeactivated", refreshClick: "refreshClick", actionClick: "actionClick", actionResponse: "actionResponse", inputChange: "inputChange", createClick: "createClick", searchClick: "searchClick", dataLoad: "dataLoad", titleActionChange: "titleActionChange" }, ngImport: i0, template: "<div class=\"row\">\n\n <div class=\"col-auto\">\n <h4>{{config.title ?? 'Untitled'}} </h4>\n </div>\n\n <div class=\"col d-flex justify-content-end align-items-center\" style=\"font-size: 14px; gap: 15px;\">\n <!-- Added: Title actions component -->\n <spa-title-actions \n *ngIf=\"config.titleActions\" \n [titleActions]=\"config.titleActions\"\n (actionChange)=\"titleActionChanged($event)\">\n </spa-title-actions>\n \n <spa-check *ngIf=\"config.searchTableConfig\" [(value)]=\"searchMode\" display=\"Search Mode\" (valueChange)=\"toggleSearch()\" style=\"margin-right: 10px;\"></spa-check>\n </div>\n\n</div>\n\n<hr style=\"margin-top: 0px;\" />\n\n\n<div style=\" font-size: 14px;\">\n <!-- Normal -->\n <spa-table *ngIf=\"!searchMode\" [config]=\"normalTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n\n <!-- Search -->\n <spa-table *ngIf=\"searchMode\" [config]=\"searchTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n</div>\n\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "component", type: TitleActionsComponent, selector: "spa-title-actions", inputs: ["titleActions"], outputs: ["actionChange"] }] }); }
|
|
17370
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PageComponent, isStandalone: false, selector: "spa-page", inputs: { config: "config" }, outputs: { searchModeActivated: "searchModeActivated", searchModeDeactivated: "searchModeDeactivated", refreshClick: "refreshClick", actionClick: "actionClick", actionResponse: "actionResponse", inputChange: "inputChange", createClick: "createClick", searchClick: "searchClick", dataLoad: "dataLoad", titleActionChange: "titleActionChange" }, ngImport: i0, template: "<div class=\"row\">\n\n <div class=\"col-auto\">\n <h4>{{config.title ?? 'Untitled'}} </h4>\n </div>\n\n <div class=\"col d-flex justify-content-end align-items-center\" style=\"font-size: 14px; gap: 15px;\">\n <!-- Added: Title actions component -->\n <spa-title-actions \n *ngIf=\"config.titleActions\" \n [titleActions]=\"config.titleActions\"\n (actionChange)=\"titleActionChanged($event)\">\n </spa-title-actions>\n \n <spa-check *ngIf=\"config.searchTableConfig\" [(value)]=\"searchMode\" display=\"Search Mode\" (valueChange)=\"toggleSearch()\" style=\"margin-right: 10px;\"></spa-check>\n </div>\n\n</div>\n\n<hr style=\"margin-top: 0px;\" />\n\n\n<div style=\" font-size: 14px;\">\n <!-- Normal -->\n <spa-table *ngIf=\"!searchMode\" [config]=\"normalTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n\n <!-- Search -->\n <spa-table *ngIf=\"searchMode\" [config]=\"searchTableConfig\" [reload]=\"tableReload\"\n (refreshClick)=\"refreshClicked()\" (actionClick)=\"actionClicked($event)\" (actionResponse)=\"actionResponded($event)\"\n (inputChange)=\"inputChanged($event)\" (createClick)=\"createClicked($event)\" (searchClick)=\"searchClicked($event)\" (dataLoad)=\"dataLoaded($event)\">\n </spa-table>\n</div>\n\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CheckComponent, selector: "spa-check", inputs: ["readonly", "display", "value", "infoMessage"], outputs: ["valueChange", "click", "check", "uncheck", "infoClick"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "component", type: TitleActionsComponent, selector: "spa-title-actions", inputs: ["titleActions"], outputs: ["actionChange"] }] }); }
|
|
16902
17371
|
}
|
|
16903
17372
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PageComponent, decorators: [{
|
|
16904
17373
|
type: Component,
|
|
@@ -17499,7 +17968,7 @@ class SpaLandingComponent {
|
|
|
17499
17968
|
if (!success)
|
|
17500
17969
|
return;
|
|
17501
17970
|
const last = await this.lastRouteService.resolve();
|
|
17502
|
-
this.
|
|
17971
|
+
this.lastRouteService.navigateTo(last ?? 'home'); // Changed: navigateTo falls back to home if the saved page no longer exists
|
|
17503
17972
|
});
|
|
17504
17973
|
}
|
|
17505
17974
|
ngAfterViewInit() {
|
|
@@ -17808,9 +18277,9 @@ class LoginComponent {
|
|
|
17808
18277
|
this.isProcessing = false;
|
|
17809
18278
|
if (success) {
|
|
17810
18279
|
this.notifications();
|
|
17811
|
-
this.
|
|
18280
|
+
this.lastRouteService.navigateTo(await this.resumeTarget(), this.redirectPath);
|
|
17812
18281
|
return;
|
|
17813
|
-
}
|
|
18282
|
+
} // Changed: navigateTo falls back to the redirect path if the saved page no longer exists
|
|
17814
18283
|
this.setupForm(); // Changed: Only set up login form if no session could be restored
|
|
17815
18284
|
});
|
|
17816
18285
|
}
|
|
@@ -17917,7 +18386,7 @@ class LoginComponent {
|
|
|
17917
18386
|
});
|
|
17918
18387
|
return;
|
|
17919
18388
|
}
|
|
17920
|
-
this.resumeTarget().then(target => this.
|
|
18389
|
+
this.resumeTarget().then(target => this.lastRouteService.navigateTo(target, this.redirectPath)); // Changed: land on the user's last page when resumeLastRoute is on, falling back to the redirect path if that page is gone
|
|
17921
18390
|
}
|
|
17922
18391
|
else if (apiResponse.message == "signup_required" && apiResponse.data) {
|
|
17923
18392
|
// Changed: Detect signup_required response and redirect to signup page
|
|
@@ -19515,7 +19984,7 @@ class PurchasingService {
|
|
|
19515
19984
|
{ value: 0, name: 'Cash' },
|
|
19516
19985
|
{ value: 1, name: 'Bank' }
|
|
19517
19986
|
];
|
|
19518
|
-
this.
|
|
19987
|
+
this.supplierPurchaseOrderItemFormConfig = {
|
|
19519
19988
|
security: { allow: [this.dataService.capInventoryReceipts] }, // Added: gate PO item form by purchasing cap (matches parent PO config)
|
|
19520
19989
|
title: 'PO Item',
|
|
19521
19990
|
fixedTitle: true,
|
|
@@ -19525,9 +19994,9 @@ class PurchasingService {
|
|
|
19525
19994
|
{ name: 'estimatedUnitCost', type: 'money', alias: 'Est. Unit Cost', required: true },
|
|
19526
19995
|
{ name: 'lineTotal', type: 'money', alias: 'Line Total', readonly: true, hideOnCreate: true }
|
|
19527
19996
|
],
|
|
19528
|
-
loadAction: { url: '
|
|
19997
|
+
loadAction: { url: 'supplierpurchaseorderitems/id' }
|
|
19529
19998
|
};
|
|
19530
|
-
this.
|
|
19999
|
+
this.supplierPurchaseOrderItemsTableConfig = {
|
|
19531
20000
|
tabTitle: 'Items',
|
|
19532
20001
|
showFilter: false,
|
|
19533
20002
|
elevation: 'none',
|
|
@@ -19542,16 +20011,16 @@ class PurchasingService {
|
|
|
19542
20011
|
{ name: 'isFullyReceived', type: 'checkbox', alias: 'Complete', icon: { name: 'check_circle', color: 'green' } }
|
|
19543
20012
|
],
|
|
19544
20013
|
buttons: [
|
|
19545
|
-
{ name: 'create', display: 'Add Item', dialog: true, action: { url: '
|
|
19546
|
-
{ name: 'edit', dialog: true, action: { url: '
|
|
19547
|
-
{ name: 'delete', dialog: true, action: { url: '
|
|
20014
|
+
{ name: 'create', display: 'Add Item', dialog: true, action: { url: 'supplierpurchaseorderitems?action=create', method: 'post' } },
|
|
20015
|
+
{ name: 'edit', dialog: true, action: { url: 'supplierpurchaseorderitems?action=edit', method: 'post' } },
|
|
20016
|
+
{ name: 'delete', dialog: true, action: { url: 'supplierpurchaseorderitems?action=delete', method: 'post' } }
|
|
19548
20017
|
],
|
|
19549
|
-
loadAction: { url: '
|
|
20018
|
+
loadAction: { url: 'supplierpurchaseorderitems/x/x' },
|
|
19550
20019
|
loadCriteria: 'po',
|
|
19551
|
-
loadIDField: '
|
|
19552
|
-
formConfig: this.
|
|
20020
|
+
loadIDField: 'supplierPurchaseOrderID',
|
|
20021
|
+
formConfig: this.supplierPurchaseOrderItemFormConfig
|
|
19553
20022
|
};
|
|
19554
|
-
this.
|
|
20023
|
+
this.supplierPurchaseOrderFormConfig = {
|
|
19555
20024
|
security: { allow: [this.dataService.capInventoryReceipts] },
|
|
19556
20025
|
title: 'Purchase Order',
|
|
19557
20026
|
fixedTitle: true,
|
|
@@ -19570,15 +20039,15 @@ class PurchasingService {
|
|
|
19570
20039
|
{ name: 'additionalInfo', type: 'section', alias: 'Additional Information', collapsed: true },
|
|
19571
20040
|
{ name: 'notes', type: 'text', alias: 'Notes', span: true, section: 'additionalInfo' }
|
|
19572
20041
|
],
|
|
19573
|
-
loadAction: { url: '
|
|
19574
|
-
heroField: '
|
|
20042
|
+
loadAction: { url: 'supplierpurchaseorders/id' },
|
|
20043
|
+
heroField: 'supplierPurchaseOrderID'
|
|
19575
20044
|
};
|
|
19576
20045
|
this.receiveGoodsFormConfig = {
|
|
19577
20046
|
security: { allow: [this.dataService.capInventoryReceipts] }, // Added: gate receive goods form by purchasing cap
|
|
19578
20047
|
title: 'Receive Goods',
|
|
19579
20048
|
multiColumn: true,
|
|
19580
20049
|
fields: [
|
|
19581
|
-
{ name: '
|
|
20050
|
+
{ name: 'supplierPurchaseOrderID', type: 'number', alias: 'PO ID', readonly: true, hidden: true },
|
|
19582
20051
|
{ name: 'poNumber', type: 'text', alias: 'PO Number', readonly: true },
|
|
19583
20052
|
{ name: 'supplierID', type: 'number', alias: 'Supplier ID', readonly: true, hidden: true },
|
|
19584
20053
|
{ name: 'supplierName', type: 'text', alias: 'Supplier', readonly: true },
|
|
@@ -19592,10 +20061,10 @@ class PurchasingService {
|
|
|
19592
20061
|
formConfig: this.receiveGoodsFormConfig,
|
|
19593
20062
|
buttons: []
|
|
19594
20063
|
};
|
|
19595
|
-
this.
|
|
19596
|
-
this.
|
|
19597
|
-
this.
|
|
19598
|
-
this.
|
|
20064
|
+
this.supplierPurchaseOrderEditButton = { name: 'edit', dialog: true, action: { url: 'supplierpurchaseorders?action=edit', method: 'post' } };
|
|
20065
|
+
this.supplierPurchaseOrderConfirmButton = { name: 'confirm', display: 'Confirm PO', inDialog: true, icon: { name: 'check_circle', color: 'blue' }, action: { url: 'supplierpurchaseorders?action=confirm', method: 'post', successMessage: 'PO Confirmed' }, confirm: { message: 'Confirm this purchase order? Items will be locked.' }, visible: (po) => po.statusName === 'Draft' };
|
|
20066
|
+
this.supplierPurchaseOrderCancelButton = { name: 'cancel', display: 'Cancel PO', inDialog: true, icon: { name: 'cancel', color: 'red' }, action: { url: 'supplierpurchaseorders?action=cancel', method: 'post', successMessage: 'PO Cancelled' }, confirm: { message: 'Cancel this purchase order?' }, visible: (po) => po.statusName !== 'FullyReceived' && po.statusName !== 'PartiallyReceived' };
|
|
20067
|
+
this.supplierPurchaseOrderReceiveButton = {
|
|
19599
20068
|
name: 'receive',
|
|
19600
20069
|
display: 'Receive Goods',
|
|
19601
20070
|
inDialog: true,
|
|
@@ -19605,14 +20074,14 @@ class PurchasingService {
|
|
|
19605
20074
|
action: { url: 'purchases?action=create', method: 'post' }, // Changed: inventoryreceipts → purchases
|
|
19606
20075
|
visible: (po) => po.statusName === 'Confirmed' || po.statusName === 'PartiallyReceived'
|
|
19607
20076
|
};
|
|
19608
|
-
this.
|
|
19609
|
-
formConfig: this.
|
|
19610
|
-
tableConfigs: [this.
|
|
19611
|
-
heroField: '
|
|
19612
|
-
buttons: [this.
|
|
20077
|
+
this.supplierPurchaseOrderDetailsConfig = {
|
|
20078
|
+
formConfig: this.supplierPurchaseOrderFormConfig,
|
|
20079
|
+
tableConfigs: [this.supplierPurchaseOrderItemsTableConfig],
|
|
20080
|
+
heroField: 'supplierPurchaseOrderID',
|
|
20081
|
+
buttons: [this.supplierPurchaseOrderEditButton, this.supplierPurchaseOrderConfirmButton, this.supplierPurchaseOrderReceiveButton, this.supplierPurchaseOrderCancelButton]
|
|
19613
20082
|
};
|
|
19614
|
-
this.
|
|
19615
|
-
this.
|
|
20083
|
+
this.supplierPurchaseOrderViewButton = { name: 'view', dialog: true, detailsConfig: this.supplierPurchaseOrderDetailsConfig };
|
|
20084
|
+
this.supplierPurchaseOrdersTableConfig = {
|
|
19616
20085
|
showFilter: true,
|
|
19617
20086
|
flatButtons: true,
|
|
19618
20087
|
minColumns: ['poNumber', 'supplierName', 'orderDate'],
|
|
@@ -19627,13 +20096,13 @@ class PurchasingService {
|
|
|
19627
20096
|
{ name: 'statusName', type: 'chip', alias: 'Status', colors: [{ name: '#E0E0E0', condition: x => x.statusName == 'Draft' }, { name: '#BBDEFB', condition: x => x.statusName == 'Confirmed' }, { name: '#FFE0B2', condition: x => x.statusName == 'PartiallyReceived' }, { name: '#C8E6C9', condition: x => x.statusName == 'FullyReceived' }, { name: '#FFCDD2', condition: x => x.statusName == 'Cancelled' }] }
|
|
19628
20097
|
],
|
|
19629
20098
|
buttons: [
|
|
19630
|
-
{ name: 'create', display: 'New Purchase Order', dialog: true, action: { url: '
|
|
19631
|
-
{ name: 'view', dialog: true, detailsConfig: this.
|
|
19632
|
-
{ name: 'delete', dialog: true, action: { url: '
|
|
20099
|
+
{ name: 'create', display: 'New Purchase Order', dialog: true, action: { url: 'supplierpurchaseorders?action=create', method: 'post' }, onSuccessButton: this.supplierPurchaseOrderViewButton },
|
|
20100
|
+
{ name: 'view', dialog: true, detailsConfig: this.supplierPurchaseOrderDetailsConfig },
|
|
20101
|
+
{ name: 'delete', dialog: true, action: { url: 'supplierpurchaseorders?action=delete', method: 'post' } }
|
|
19633
20102
|
],
|
|
19634
|
-
loadAction: { url: '
|
|
19635
|
-
formConfig: this.
|
|
19636
|
-
entityName: '
|
|
20103
|
+
loadAction: { url: 'supplierpurchaseorders/all/x' },
|
|
20104
|
+
formConfig: this.supplierPurchaseOrderFormConfig,
|
|
20105
|
+
entityName: 'SupplierPurchaseOrder'
|
|
19637
20106
|
};
|
|
19638
20107
|
//--------------------------Purchases (renamed from Inventory Receipts)-------------------------
|
|
19639
20108
|
// Changed: Renamed from inventoryReceiptItemFormConfig to purchaseItemFormConfig
|
|
@@ -19709,7 +20178,7 @@ class PurchasingService {
|
|
|
19709
20178
|
},
|
|
19710
20179
|
{ name: 'purchaseNumber', type: 'text', alias: 'Purchase Number', readonly: true, hideOnCreate: true, section: 'purchaseInfo', infoMessage: 'Auto-generated unique purchase identifier' }, // Changed: receiptNumber → purchaseNumber
|
|
19711
20180
|
{ name: 'poNumber', type: 'text', alias: 'PO Number', hideOnCreate: true, section: 'purchaseInfo', infoMessage: 'Linked purchase order if receiving against a PO', // Changed: receiptInfo → purchaseInfo
|
|
19712
|
-
hidden: (row) => !row.
|
|
20181
|
+
hidden: (row) => !row.supplierPurchaseOrderID,
|
|
19713
20182
|
},
|
|
19714
20183
|
{ name: 'multipleProducts', type: 'checkbox', alias: 'Multiple Products', defaultValue: false, hideOnExists: true, infoMessage: 'Check this box if you want to add multiple products to this purchase' }, // Changed: receipt → purchase
|
|
19715
20184
|
{ name: 'quickPurchaseItem', type: 'section', alias: 'Quick Purchase Item',
|
|
@@ -22985,22 +23454,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
22985
23454
|
}]
|
|
22986
23455
|
}] });
|
|
22987
23456
|
|
|
22988
|
-
class
|
|
23457
|
+
class SupplierPurchaseOrdersComponent {
|
|
22989
23458
|
constructor() {
|
|
22990
23459
|
this.purchasingService = inject(PurchasingService); // Changed: Inject PurchasingService
|
|
22991
23460
|
this.pageConfig = {
|
|
22992
23461
|
title: 'Purchase Orders',
|
|
22993
|
-
tableConfig: this.purchasingService.
|
|
23462
|
+
tableConfig: this.purchasingService.supplierPurchaseOrdersTableConfig // Changed: Use PurchasingService config
|
|
22994
23463
|
};
|
|
22995
23464
|
}
|
|
22996
23465
|
ngOnInit() { }
|
|
22997
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type:
|
|
22998
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type:
|
|
23466
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SupplierPurchaseOrdersComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
23467
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SupplierPurchaseOrdersComponent, isStandalone: false, selector: "spa-supplier-purchase-orders", ngImport: i0, template: '<spa-page [config]="pageConfig"></spa-page>', isInline: true, dependencies: [{ kind: "component", type: PageComponent, selector: "spa-page", inputs: ["config"], outputs: ["searchModeActivated", "searchModeDeactivated", "refreshClick", "actionClick", "actionResponse", "inputChange", "createClick", "searchClick", "dataLoad", "titleActionChange"] }] }); }
|
|
22999
23468
|
}
|
|
23000
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type:
|
|
23469
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SupplierPurchaseOrdersComponent, decorators: [{
|
|
23001
23470
|
type: Component,
|
|
23002
23471
|
args: [{
|
|
23003
|
-
selector: 'spa-purchase-orders',
|
|
23472
|
+
selector: 'spa-supplier-purchase-orders',
|
|
23004
23473
|
template: '<spa-page [config]="pageConfig"></spa-page>',
|
|
23005
23474
|
standalone: false
|
|
23006
23475
|
}]
|
|
@@ -23091,7 +23560,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
23091
23560
|
}] });
|
|
23092
23561
|
|
|
23093
23562
|
const PURCHASING_ROUTES = [
|
|
23094
|
-
{ path: "orders", component:
|
|
23563
|
+
{ path: "orders", component: SupplierPurchaseOrdersComponent },
|
|
23095
23564
|
{ path: "purchases", component: InventoryReceiptsComponent },
|
|
23096
23565
|
{ path: "dashboard", component: PurchasingDashboardComponent }
|
|
23097
23566
|
];
|
|
@@ -24655,7 +25124,7 @@ class TasksComponent {
|
|
|
24655
25124
|
});
|
|
24656
25125
|
}
|
|
24657
25126
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TasksComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: AuthService }, { token: i4.MatDialog }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
24658
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TasksComponent, isStandalone: false, selector: "spa-tasks", ngImport: i0, template: "<div class=\"d-flex align-items-center justify-content-between mt-0\" style=\"margin-left: 10px\">\n\n <label style=\"font-size: 16px;\">Tasks</label>\n\n <div>\n <button mat-mini-fab color=\"primary\" style=\"margin-right:1em;\" (click)=\"cats()\" matTooltip=\"Categories\" matTooltipPosition=\"above\"><mat-icon>category</mat-icon></button>\n </div>\n\n</div>\n<hr>\n\n<div class=\"mt-3\" style=\" font-size: 14px;\">\n <spa-table [config]=\"tasksTableConfig\" [reload]=\"reload\"></spa-table>\n</div>\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"], dependencies: [{ kind: "component", type: i3.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
25127
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TasksComponent, isStandalone: false, selector: "spa-tasks", ngImport: i0, template: "<div class=\"d-flex align-items-center justify-content-between mt-0\" style=\"margin-left: 10px\">\n\n <label style=\"font-size: 16px;\">Tasks</label>\n\n <div>\n <button mat-mini-fab color=\"primary\" style=\"margin-right:1em;\" (click)=\"cats()\" matTooltip=\"Categories\" matTooltipPosition=\"above\"><mat-icon>category</mat-icon></button>\n </div>\n\n</div>\n<hr>\n\n<div class=\"mt-3\" style=\" font-size: 14px;\">\n <spa-table [config]=\"tasksTableConfig\" [reload]=\"reload\"></spa-table>\n</div>\n", styles: [".mat-mini-fab{width:32px;height:32px}.mat-mini-fab mat-icon{font-size:16px;margin-top:-3px}\n"], dependencies: [{ kind: "component", type: i3.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i7$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
24659
25128
|
}
|
|
24660
25129
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TasksComponent, decorators: [{
|
|
24661
25130
|
type: Component,
|
|
@@ -24896,7 +25365,7 @@ class TenantSettingsComponent {
|
|
|
24896
25365
|
this.authService.logoff();
|
|
24897
25366
|
}
|
|
24898
25367
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantSettingsComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: AuthService }, { token: i4.MatDialog }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
24899
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TenantSettingsComponent, isStandalone: false, selector: "spa-tenant-settings", ngImport: i0, template: "<div class=\"container\">\n\n <div>\n\n <label class=\"title\" >Organisation Details</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n\n <div *ngIf=\"currTenant && plan\" class=\"mb-2 mt-3 tin-grid\" style=\" font-size: 14px;\">\n\n <div class=\"tin-col mb-3\" style=\"max-width: 500px;\">\n <spa-select display=\"Current Organisation\" [options]=\"tenants\" optionDisplay=\"name\" optionValue=\"tenantID\" [(value)]=\"currentTenantID\"\n hint=\"You are required to login again after switching organisations.\" style=\"min-width: 300px;margin-bottom: 10px;\"></spa-select>\n <button mat-stroked-button color=\"primary\" [disabled]=\"currentTenantID == currTenant.tenantID\" (click)=\"switchTenant()\">Switch</button>\n </div>\n\n </div>\n\n </div>\n\n\n <ng-container *ngIf=\"ownTenant\" >\n <!-- Members -->\n <div class=\"mt-3\" >\n\n <label class=\"title\" >Members</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Invite other users to join your organisation as partners or employees to form a partnership or company.</label>\n\n <spa-table [config]=\"membersTableConfig\" [reload]=\"tableReload\" ></spa-table>\n\n </div>\n\n\n<!-- My Organisations -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\" >My Organisations</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Organisations that you are a member of.</label>\n\n <spa-table [config]=\"orgsTableConfig\" [reload]=\"orgsReload\" (actionResponse)=\"updateTenant($event)\"></spa-table>\n\n </div>\n\n\n <!-- My Invitations -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\">My Invitations</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Requests for you to join other organisations.</label>\n\n\n <spa-invitations-table></spa-invitations-table>\n\n </div>\n\n <!-- Billing -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\" >Billing and Subscription</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n\n <div *ngIf=\"currTenant && plan\" class=\"mb-1 mt-3\" style=\"max-width: 300px; font-size: 14px;\">\n <spa-label display=\"Plan\" [value]=\"plan.name\"></spa-label>\n <spa-label display=\"Next Payment\" format=\"money\" [value]=\"plan.price\"></spa-label>\n <spa-label display=\"Due Date\" format=\"date\" value=\"2024-01-01\"></spa-label>\n </div>\n\n </div>\n\n <!-- Email -->\n <div class=\"mt-3 mb-5\" *ngIf=\"ownTenant\">\n\n <label class=\"title\">Email Configuration</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Configure email settings for sending notifications.</label>\n\n <spa-table [config]=\"mailerTableConfig\"></spa-table>\n\n </div>\n\n\n </ng-container>\n\n\n\n</div>\n\n\n", styles: [".title{margin-top:1em;font-size:28px;font-weight:300}.subtitle{font-size:smaller}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: LabelComponent, selector: "spa-label", inputs: ["display", "value", "format", "suffix", "size"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "component", type: InvitationsTableComponent, selector: "spa-invitations-table" }] }); }
|
|
25368
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TenantSettingsComponent, isStandalone: false, selector: "spa-tenant-settings", ngImport: i0, template: "<div class=\"container\">\n\n <div>\n\n <label class=\"title\" >Organisation Details</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n\n <div *ngIf=\"currTenant && plan\" class=\"mb-2 mt-3 tin-grid\" style=\" font-size: 14px;\">\n\n <div class=\"tin-col mb-3\" style=\"max-width: 500px;\">\n <spa-select display=\"Current Organisation\" [options]=\"tenants\" optionDisplay=\"name\" optionValue=\"tenantID\" [(value)]=\"currentTenantID\"\n hint=\"You are required to login again after switching organisations.\" style=\"min-width: 300px;margin-bottom: 10px;\"></spa-select>\n <button mat-stroked-button color=\"primary\" [disabled]=\"currentTenantID == currTenant.tenantID\" (click)=\"switchTenant()\">Switch</button>\n </div>\n\n </div>\n\n </div>\n\n\n <ng-container *ngIf=\"ownTenant\" >\n <!-- Members -->\n <div class=\"mt-3\" >\n\n <label class=\"title\" >Members</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Invite other users to join your organisation as partners or employees to form a partnership or company.</label>\n\n <spa-table [config]=\"membersTableConfig\" [reload]=\"tableReload\" ></spa-table>\n\n </div>\n\n\n<!-- My Organisations -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\" >My Organisations</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Organisations that you are a member of.</label>\n\n <spa-table [config]=\"orgsTableConfig\" [reload]=\"orgsReload\" (actionResponse)=\"updateTenant($event)\"></spa-table>\n\n </div>\n\n\n <!-- My Invitations -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\">My Invitations</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Requests for you to join other organisations.</label>\n\n\n <spa-invitations-table></spa-invitations-table>\n\n </div>\n\n <!-- Billing -->\n <div class=\"mt-3\" *ngIf=\"ownTenant\">\n\n <label class=\"title\" >Billing and Subscription</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n\n <div *ngIf=\"currTenant && plan\" class=\"mb-1 mt-3\" style=\"max-width: 300px; font-size: 14px;\">\n <spa-label display=\"Plan\" [value]=\"plan.name\"></spa-label>\n <spa-label display=\"Next Payment\" format=\"money\" [value]=\"plan.price\"></spa-label>\n <spa-label display=\"Due Date\" format=\"date\" value=\"2024-01-01\"></spa-label>\n </div>\n\n </div>\n\n <!-- Email -->\n <div class=\"mt-3 mb-5\" *ngIf=\"ownTenant\">\n\n <label class=\"title\">Email Configuration</label>\n <hr style=\"margin-top: 0px; margin-bottom: 0px;\">\n <label class=\"subtitle text-muted\">Configure email settings for sending notifications.</label>\n\n <spa-table [config]=\"mailerTableConfig\"></spa-table>\n\n </div>\n\n\n </ng-container>\n\n\n\n</div>\n\n\n", styles: [".title{margin-top:1em;font-size:28px;font-weight:300}.subtitle{font-size:smaller}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: SelectComponent, selector: "spa-select", inputs: ["detailsConfig"] }, { kind: "component", type: LabelComponent, selector: "spa-label", inputs: ["display", "value", "format", "suffix", "size"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }, { kind: "component", type: InvitationsTableComponent, selector: "spa-invitations-table" }] }); }
|
|
24900
25369
|
}
|
|
24901
25370
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TenantSettingsComponent, decorators: [{
|
|
24902
25371
|
type: Component,
|
|
@@ -26068,7 +26537,7 @@ class ApprovalsComponent {
|
|
|
26068
26537
|
ngOnInit() {
|
|
26069
26538
|
}
|
|
26070
26539
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ApprovalsComponent, deps: [{ token: DataServiceLib }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
26071
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: ApprovalsComponent, isStandalone: false, selector: "spa-approvals", ngImport: i0, template: "<h4>Approvals</h4>\n<hr>\n\n<mat-tab-group>\n <mat-tab label=\"Received\">\n <div class=\"mt-3\">\n <spa-table [config]=\"dataService.receivedApprovalsTableConfig\"></spa-table>\n </div>\n </mat-tab>\n <mat-tab label=\"Sent\">\n <div class=\"mt-3\">\n <spa-table [config]=\"dataService.sentApprovalsTableConfig\"></spa-table>\n </div>\n </mat-tab>\n</mat-tab-group>\n", styles: [""], dependencies: [{ kind: "component", type: i5$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
26540
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: ApprovalsComponent, isStandalone: false, selector: "spa-approvals", ngImport: i0, template: "<h4>Approvals</h4>\n<hr>\n\n<mat-tab-group>\n <mat-tab label=\"Received\">\n <div class=\"mt-3\">\n <spa-table [config]=\"dataService.receivedApprovalsTableConfig\"></spa-table>\n </div>\n </mat-tab>\n <mat-tab label=\"Sent\">\n <div class=\"mt-3\">\n <spa-table [config]=\"dataService.sentApprovalsTableConfig\"></spa-table>\n </div>\n </mat-tab>\n</mat-tab-group>\n", styles: [""], dependencies: [{ kind: "component", type: i5$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i5$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: TableComponent, selector: "spa-table", inputs: ["data", "tileData", "config", "localMode", "parentDetails", "reload", "activeTab", "inTab", "nestingLevel"], outputs: ["dataLoad", "totalChange", "actionSuccess", "refreshClick", "searchClick", "createClick", "actionClick", "inputChange", "actionResponse"] }] }); }
|
|
26072
26541
|
}
|
|
26073
26542
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ApprovalsComponent, decorators: [{
|
|
26074
26543
|
type: Component,
|
|
@@ -26914,7 +27383,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
26914
27383
|
|
|
26915
27384
|
class PurchasingModule {
|
|
26916
27385
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PurchasingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
26917
|
-
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.14", ngImport: i0, type: PurchasingModule, declarations: [
|
|
27386
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.14", ngImport: i0, type: PurchasingModule, declarations: [SupplierPurchaseOrdersComponent,
|
|
26918
27387
|
InventoryReceiptsComponent,
|
|
26919
27388
|
PurchasingDashboardComponent], imports: [CommonModule,
|
|
26920
27389
|
SpaAdminModule] }); }
|
|
@@ -26925,7 +27394,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
26925
27394
|
type: NgModule,
|
|
26926
27395
|
args: [{
|
|
26927
27396
|
declarations: [
|
|
26928
|
-
|
|
27397
|
+
SupplierPurchaseOrdersComponent,
|
|
26929
27398
|
InventoryReceiptsComponent,
|
|
26930
27399
|
PurchasingDashboardComponent
|
|
26931
27400
|
],
|