codeapp-js 0.1.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/codeApp/dist/codeapp.js +552 -78
  2. package/codeApp/dist/power-apps-data.js +2531 -2531
  3. package/dev files/outlook.js +218 -9
  4. package/examples/combined demo/dist/codeapp.js +1098 -0
  5. package/examples/combined demo/dist/index.js +69 -114
  6. package/examples/combined demo/dist/power-apps-data.js +646 -170
  7. package/examples/combined demo/power.config.json +42 -42
  8. package/examples/dataverse Demo/dist/codeapp.js +1085 -0
  9. package/examples/dataverse Demo/dist/index.html +54 -54
  10. package/examples/dataverse Demo/dist/index.js +82 -70
  11. package/examples/dataverse Demo/dist/power-apps-data.js +551 -170
  12. package/examples/dataverse Demo/power.config.json +34 -34
  13. package/examples/dataverse Demo/readme.md +79 -79
  14. package/examples/groups Demo/dist/codeapp.js +1085 -0
  15. package/examples/groups Demo/dist/index.js +27 -27
  16. package/examples/groups Demo/dist/power-apps-data.js +551 -170
  17. package/examples/kanban/dist/dataverse.js +94 -94
  18. package/examples/kanban/dist/environmentVar.js +55 -55
  19. package/examples/kanban/dist/office365groups.js +97 -97
  20. package/examples/kanban/dist/office365users.js +169 -169
  21. package/examples/kanban/dist/outlook.js +162 -162
  22. package/examples/kanban/dist/power-apps-data.js +560 -138
  23. package/examples/kanban/dist/sharepoint.js +339 -339
  24. package/examples/myProfile/dist/index.html +184 -184
  25. package/examples/myProfile/dist/index.js +141 -141
  26. package/examples/myProfile/dist/office365users.js +169 -169
  27. package/examples/myProfile/dist/power-apps-data.js +560 -138
  28. package/examples/myProfile/power.config.json +22 -22
  29. package/examples/myProfile/readme.md +79 -79
  30. package/examples/outlook Demo/dist/codeapp.js +1085 -0
  31. package/examples/outlook Demo/dist/index.html +35 -35
  32. package/examples/outlook Demo/dist/index.js +170 -166
  33. package/examples/outlook Demo/dist/outlook.js +121 -121
  34. package/examples/outlook Demo/dist/power-apps-data.js +551 -170
  35. package/examples/outlook Demo/dist/styles.css +84 -84
  36. package/examples/outlook Demo/readme.md +82 -82
  37. package/examples/outlook Demo2/OutlookDemo_1_0_0_1.zip +0 -0
  38. package/examples/outlook Demo2/agent/decision-log.md +7 -0
  39. package/examples/outlook Demo2/dist/codeapp.js +1334 -0
  40. package/examples/outlook Demo2/dist/icon-512.png +0 -0
  41. package/examples/outlook Demo2/dist/index.html +98 -0
  42. package/examples/outlook Demo2/dist/index.js +346 -0
  43. package/examples/outlook Demo2/dist/power-apps-data.js +3007 -0
  44. package/examples/outlook Demo2/dist/styles.css +639 -0
  45. package/examples/outlook Demo2/power.config.json +23 -0
  46. package/examples/outlook Demo2/src/generated/index.ts +14 -0
  47. package/examples/outlook Demo2/src/generated/models/Office365GroupsModel.ts +363 -0
  48. package/examples/outlook Demo2/src/generated/models/Office365OutlookModel.ts +2046 -0
  49. package/examples/outlook Demo2/src/generated/models/Office365UsersModel.ts +254 -0
  50. package/examples/outlook Demo2/src/generated/services/Office365GroupsService.ts +326 -0
  51. package/examples/outlook Demo2/src/generated/services/Office365OutlookService.ts +2476 -0
  52. package/examples/outlook Demo2/src/generated/services/Office365UsersService.ts +358 -0
  53. package/examples/planning Poker/.vscode/settings.json +4 -4
  54. package/examples/planning Poker/additional files/customizations (tables).xml +6428 -6428
  55. package/examples/planning Poker/additional files/dataverse-tables.json +165 -165
  56. package/examples/planning Poker/additional files/readme.md +122 -122
  57. package/examples/planning Poker/dist/dataverse.js +78 -78
  58. package/examples/planning Poker/dist/index.html +198 -198
  59. package/examples/planning Poker/dist/index.js +954 -954
  60. package/examples/planning Poker/dist/power-apps-data.js +560 -138
  61. package/examples/planning Poker/dist/styles.css +815 -815
  62. package/examples/sharePoint Demo/agent/decision-log.md +5 -5
  63. package/examples/sharePoint Demo/dist/codeapp.js +1085 -0
  64. package/examples/sharePoint Demo/dist/index.js +44 -51
  65. package/examples/sharePoint Demo/dist/power-apps-data.js +551 -170
  66. package/examples/sharePoint Demo/power.config.json +22 -22
  67. package/examples/solution explorer/agent/decision-log.md +27 -0
  68. package/examples/solution explorer/agent/mockup-01-swiss-grid.html +452 -0
  69. package/examples/solution explorer/agent/mockup-02-dark-glass.html +496 -0
  70. package/examples/solution explorer/agent/mockup-03-paper-console.html +510 -0
  71. package/examples/solution explorer/agent/mockup-04-neon-noir.html +546 -0
  72. package/examples/solution explorer/agent/mockup-05-zen-garden.html +534 -0
  73. package/examples/solution explorer/dist/codeapp.js +1098 -0
  74. package/examples/solution explorer/dist/icon-512.png +0 -0
  75. package/examples/solution explorer/dist/index.html +80 -0
  76. package/examples/solution explorer/dist/index.js +735 -0
  77. package/examples/solution explorer/dist/power-apps-data.js +3007 -0
  78. package/examples/solution explorer/dist/styles.css +571 -0
  79. package/examples/solution explorer/power.config.json +151 -0
  80. package/examples/todo/dist/dataverse.js +64 -64
  81. package/examples/todo/dist/index.html +75 -75
  82. package/examples/todo/dist/index.js +8 -8
  83. package/examples/todo/dist/power-apps-data.js +560 -138
  84. package/examples/todo/dist/renderer.js +375 -375
  85. package/examples/todo/dist/styles.css +691 -691
  86. package/examples/todo/power.config.json +34 -34
  87. package/package.json +1 -8
  88. package/docs-mockups/atelier/index.html +0 -120
  89. package/docs-mockups/atelier/script.js +0 -23
  90. package/docs-mockups/atelier/styles.css +0 -361
  91. package/docs-mockups/field-guide/index.html +0 -112
  92. package/docs-mockups/field-guide/script.js +0 -20
  93. package/docs-mockups/field-guide/styles.css +0 -272
  94. package/docs-mockups/index.html +0 -80
  95. package/docs-mockups/maker-hub/index.html +0 -178
  96. package/docs-mockups/maker-hub/script.js +0 -20
  97. package/docs-mockups/maker-hub/styles.css +0 -404
  98. package/docs-mockups/script.js +0 -26
  99. package/docs-mockups/signal/index.html +0 -146
  100. package/docs-mockups/signal/script.js +0 -20
  101. package/docs-mockups/signal/styles.css +0 -314
  102. package/docs-mockups/styles.css +0 -287
  103. package/examples/combined demo/dist/dataverse.js +0 -86
  104. package/examples/combined demo/dist/environmentVar.js +0 -55
  105. package/examples/combined demo/dist/office365groups.js +0 -97
  106. package/examples/combined demo/dist/office365users.js +0 -169
  107. package/examples/combined demo/dist/outlook.js +0 -162
  108. package/examples/combined demo/dist/sharepoint.js +0 -339
  109. package/examples/dataverse Demo/dist/dataverse.js +0 -86
  110. package/examples/groups Demo/dist/dataverse.js +0 -86
  111. package/examples/groups Demo/dist/environmentVar.js +0 -55
  112. package/examples/groups Demo/dist/office365groups.js +0 -97
  113. package/examples/groups Demo/dist/office365users.js +0 -169
  114. package/examples/groups Demo/dist/outlook.js +0 -162
  115. package/examples/groups Demo/dist/sharepoint.js +0 -339
  116. package/examples/sharePoint Demo/dist/dataverse.js +0 -94
  117. package/examples/sharePoint Demo/dist/environmentVar.js +0 -55
  118. package/examples/sharePoint Demo/dist/office365groups.js +0 -97
  119. package/examples/sharePoint Demo/dist/office365users.js +0 -169
  120. package/examples/sharePoint Demo/dist/outlook.js +0 -162
  121. package/examples/sharePoint Demo/dist/sharepoint.js +0 -339
@@ -0,0 +1,98 @@
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Outlook Mail</title>
8
+ <link rel="stylesheet" href="styles.css" />
9
+ <script type="module" src="power-apps-data.js"></script>
10
+ <script type="module" src="codeapp.js"></script>
11
+ <script type="module" src="index.js"></script>
12
+ </head>
13
+ <body>
14
+ <div id="root">
15
+ <!-- Header -->
16
+ <header class="app-header">
17
+ <div class="app-header__brand">
18
+ <div class="app-header__icon">M</div>
19
+ <h1 class="app-header__title">Mail</h1>
20
+ </div>
21
+ <div class="app-header__actions">
22
+ <button class="btn btn--secondary" id="btnRefresh" title="Refresh inbox">
23
+ ↻ Refresh
24
+ </button>
25
+ <button class="btn btn--primary" id="btnCompose" title="Compose new email">
26
+ ✎ Compose
27
+ </button>
28
+ </div>
29
+ </header>
30
+
31
+ <!-- Body -->
32
+ <div class="app-body" id="appBody">
33
+ <!-- Inbox List -->
34
+ <aside class="inbox-panel">
35
+ <div class="inbox-panel__header">
36
+ <span class="inbox-panel__title">Inbox</span>
37
+ <span class="inbox-panel__count" id="emailCount">—</span>
38
+ </div>
39
+ <div class="inbox-panel__list" id="emailList">
40
+ <!-- Skeleton loader -->
41
+ <div class="loading-skeleton" id="loadingSkeleton">
42
+ <div class="skeleton-item"><div class="skeleton-circle"></div><div class="skeleton-lines"><div class="skeleton-line skeleton-line--short"></div><div class="skeleton-line skeleton-line--medium"></div><div class="skeleton-line"></div></div></div>
43
+ <div class="skeleton-item"><div class="skeleton-circle"></div><div class="skeleton-lines"><div class="skeleton-line skeleton-line--short"></div><div class="skeleton-line skeleton-line--medium"></div><div class="skeleton-line"></div></div></div>
44
+ <div class="skeleton-item"><div class="skeleton-circle"></div><div class="skeleton-lines"><div class="skeleton-line skeleton-line--short"></div><div class="skeleton-line skeleton-line--medium"></div><div class="skeleton-line"></div></div></div>
45
+ <div class="skeleton-item"><div class="skeleton-circle"></div><div class="skeleton-lines"><div class="skeleton-line skeleton-line--short"></div><div class="skeleton-line skeleton-line--medium"></div><div class="skeleton-line"></div></div></div>
46
+ <div class="skeleton-item"><div class="skeleton-circle"></div><div class="skeleton-lines"><div class="skeleton-line skeleton-line--short"></div><div class="skeleton-line skeleton-line--medium"></div><div class="skeleton-line"></div></div></div>
47
+ </div>
48
+ </div>
49
+ </aside>
50
+
51
+ <!-- Detail Panel -->
52
+ <main class="detail-panel" id="detailPanel">
53
+ <div class="detail-panel__empty" id="detailEmpty">
54
+ <div class="detail-panel__empty-icon">✉</div>
55
+ <div class="detail-panel__empty-text">Select an email to read</div>
56
+ </div>
57
+ <div class="detail-panel__content" id="detailContent" style="display:none;"></div>
58
+ </main>
59
+ </div>
60
+
61
+ <!-- Status Bar -->
62
+ <div class="status-bar" id="statusBar">
63
+ <div class="status-bar__dot"></div>
64
+ <span id="statusText">Ready</span>
65
+ </div>
66
+
67
+ <!-- Compose Modal -->
68
+ <div class="modal-overlay" id="composeOverlay">
69
+ <div class="compose-modal">
70
+ <div class="compose-modal__header">
71
+ <h2 class="compose-modal__title">New Message</h2>
72
+ <button class="compose-modal__close" id="btnCloseCompose">✕</button>
73
+ </div>
74
+ <div class="compose-modal__body">
75
+ <div class="form-group">
76
+ <label class="form-group__label" for="composeTo">To</label>
77
+ <input class="form-group__input" type="email" id="composeTo" placeholder="recipient@example.com" />
78
+ </div>
79
+ <div class="form-group">
80
+ <label class="form-group__label" for="composeSubject">Subject</label>
81
+ <input class="form-group__input" type="text" id="composeSubject" placeholder="What is this about?" />
82
+ </div>
83
+ <div class="form-group">
84
+ <label class="form-group__label" for="composeBody">Message</label>
85
+ <textarea class="form-group__input form-group__input--textarea" id="composeBody" placeholder="Write your message…"></textarea>
86
+ </div>
87
+ </div>
88
+ <div class="compose-modal__footer">
89
+ <button class="btn btn--ghost" id="btnDiscardCompose">Discard</button>
90
+ <button class="btn btn--primary" id="btnSendEmail">
91
+ Send ➤
92
+ </button>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </body>
98
+ </html>
@@ -0,0 +1,346 @@
1
+
2
+ import { getClient } from './power-apps-data.js';
3
+
4
+ // ===== DATA SOURCE & API METADATA =====
5
+ const DATA_SOURCE = 'office365';
6
+
7
+ const OUTLOOK_APIS = {
8
+ GetEmailsV3: {
9
+ path: '/{connectionId}/v3/Mail',
10
+ method: 'GET',
11
+ parameters: [
12
+ { name: 'connectionId', in: 'path', required: true },
13
+ { name: 'folderPath', in: 'query', required: false },
14
+ { name: 'fetchOnlyUnread', in: 'query', required: false },
15
+ { name: 'searchQuery', in: 'query', required: false },
16
+ { name: 'top', in: 'query', required: false }
17
+ ]
18
+ },
19
+ SendEmailV2: {
20
+ path: '/{connectionId}/v2/Mail',
21
+ method: 'POST',
22
+ parameters: [
23
+ { name: 'connectionId', in: 'path', required: true },
24
+ { name: 'emailMessage', in: 'body', required: true }
25
+ ]
26
+ }
27
+ };
28
+
29
+ const ALL_DATA_SOURCES = {
30
+ [DATA_SOURCE]: {
31
+ tableId: '',
32
+ version: '',
33
+ primaryKey: '',
34
+ dataSourceType: 'Connector',
35
+ apis: OUTLOOK_APIS
36
+ }
37
+ };
38
+
39
+ // ===== SINGLE SHARED CLIENT =====
40
+ let _client = null;
41
+ const getSharedClient = () => {
42
+ if (!_client) {
43
+ _client = getClient(ALL_DATA_SOURCES);
44
+ }
45
+ return _client;
46
+ };
47
+
48
+ // ===== CONNECTOR HELPER =====
49
+ const execConnector = async (tableName, operationName, parameters) => {
50
+ const client = getSharedClient();
51
+ const result = await client.executeAsync({
52
+ connectorOperation: {
53
+ tableName,
54
+ operationName,
55
+ parameters
56
+ }
57
+ });
58
+ return unwrapResult(result);
59
+ };
60
+
61
+ const unwrapResult = (result) => {
62
+ if (!result.success) {
63
+ const msg = result.error?.message || 'Operation failed';
64
+ throw new Error(msg);
65
+ }
66
+ // Normalize: could be array, { value: [] }, or direct data
67
+ const data = result.data;
68
+ if (Array.isArray(data)) return data;
69
+ if (data && Array.isArray(data.value)) return data.value;
70
+ return data;
71
+ };
72
+
73
+ // ===== STATE =====
74
+ let emails = [];
75
+ let selectedEmailId = null;
76
+
77
+ // ===== DOM REFS =====
78
+ const $ = (sel) => document.querySelector(sel);
79
+ const emailListEl = () => $('#emailList');
80
+ const emailCountEl = () => $('#emailCount');
81
+ const loadingEl = () => $('#loadingSkeleton');
82
+ const detailEmpty = () => $('#detailEmpty');
83
+ const detailContent = () => $('#detailContent');
84
+ const statusBar = () => $('#statusBar');
85
+ const statusText = () => $('#statusText');
86
+ const composeOverlay = () => $('#composeOverlay');
87
+
88
+ // ===== STATUS HELPERS =====
89
+ const setStatus = (text, type = 'ok') => {
90
+ const bar = statusBar();
91
+ const txt = statusText();
92
+ if (!bar || !txt) return;
93
+ bar.className = 'status-bar' + (type === 'error' ? ' status-bar--error' : type === 'loading' ? ' status-bar--loading' : '');
94
+ txt.textContent = text;
95
+ };
96
+
97
+ // ===== FORMAT HELPERS =====
98
+ const getInitials = (name) => {
99
+ if (!name) return '?';
100
+ const parts = name.trim().split(/\s+/);
101
+ if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
102
+ return name.slice(0, 2).toUpperCase();
103
+ };
104
+
105
+ const formatTime = (dateStr) => {
106
+ if (!dateStr) return '';
107
+ try {
108
+ const d = new Date(dateStr);
109
+ const now = new Date();
110
+ const diff = now - d;
111
+ const oneDay = 86400000;
112
+ if (diff < oneDay && d.getDate() === now.getDate()) {
113
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
114
+ }
115
+ if (diff < 7 * oneDay) {
116
+ return d.toLocaleDateString([], { weekday: 'short' });
117
+ }
118
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
119
+ } catch {
120
+ return '';
121
+ }
122
+ };
123
+
124
+ const formatFullDate = (dateStr) => {
125
+ if (!dateStr) return '';
126
+ try {
127
+ return new Date(dateStr).toLocaleString([], {
128
+ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
129
+ hour: '2-digit', minute: '2-digit'
130
+ });
131
+ } catch {
132
+ return '';
133
+ }
134
+ };
135
+
136
+ const stripHtml = (html) => {
137
+ if (!html) return '';
138
+ const tmp = document.createElement('div');
139
+ tmp.innerHTML = html;
140
+ return tmp.textContent || tmp.innerText || '';
141
+ };
142
+
143
+ // ===== RENDER INBOX =====
144
+ const renderEmailList = () => {
145
+ const list = emailListEl();
146
+ if (!list) return;
147
+
148
+ const skeleton = loadingEl();
149
+ if (skeleton) skeleton.remove();
150
+
151
+ if (emails.length === 0) {
152
+ list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--ink-muted);font-size:14px;">No emails found</div>';
153
+ emailCountEl().textContent = '0';
154
+ return;
155
+ }
156
+
157
+ emailCountEl().textContent = `${emails.length}`;
158
+
159
+ list.innerHTML = emails.map((email) => {
160
+ const id = email.Id || email.id || '';
161
+ const from = email.From || email.from;
162
+ const senderName = from?.EmailAddress?.Name || from?.emailAddress?.name || from?.Name || 'Unknown';
163
+ const subject = email.Subject || email.subject || '(No subject)';
164
+ const preview = stripHtml(email.BodyPreview || email.bodyPreview || email.Body?.Content || '');
165
+ const dateStr = email.DateTimeReceived || email.receivedDateTime || email.ReceivedDateTime || '';
166
+ const isRead = email.IsRead ?? email.isRead ?? true;
167
+ const isActive = id === selectedEmailId;
168
+
169
+ return `
170
+ <div class="email-item ${!isRead ? 'email-item--unread' : ''} ${isActive ? 'email-item--active' : ''}"
171
+ data-id="${id}" role="button" tabindex="0">
172
+ <div class="email-item__avatar">${getInitials(senderName)}</div>
173
+ <div class="email-item__content">
174
+ <div class="email-item__row">
175
+ <span class="email-item__sender">${escapeHtml(senderName)}</span>
176
+ <span class="email-item__time">${formatTime(dateStr)}</span>
177
+ </div>
178
+ <div class="email-item__subject">${escapeHtml(subject)}</div>
179
+ <div class="email-item__preview">${escapeHtml(preview.slice(0, 100))}</div>
180
+ </div>
181
+ </div>
182
+ `;
183
+ }).join('');
184
+
185
+ // Attach click handlers
186
+ list.querySelectorAll('.email-item').forEach((el) => {
187
+ el.addEventListener('click', () => selectEmail(el.dataset.id));
188
+ });
189
+ };
190
+
191
+ const escapeHtml = (str) => {
192
+ const div = document.createElement('div');
193
+ div.appendChild(document.createTextNode(str || ''));
194
+ return div.innerHTML;
195
+ };
196
+
197
+ // ===== SELECT EMAIL =====
198
+ const selectEmail = (id) => {
199
+ selectedEmailId = id;
200
+ const email = emails.find((e) => (e.Id || e.id) === id);
201
+ if (!email) return;
202
+
203
+ // Re-render list to update active state
204
+ renderEmailList();
205
+
206
+ // Show detail
207
+ const empty = detailEmpty();
208
+ const content = detailContent();
209
+ if (empty) empty.style.display = 'none';
210
+ if (!content) return;
211
+ content.style.display = 'block';
212
+
213
+ const from = email.From || email.from;
214
+ const senderName = from?.EmailAddress?.Name || from?.emailAddress?.name || from?.Name || 'Unknown';
215
+ const senderEmail = from?.EmailAddress?.Address || from?.emailAddress?.address || from?.Address || '';
216
+ const subject = email.Subject || email.subject || '(No subject)';
217
+ const dateStr = email.DateTimeReceived || email.receivedDateTime || email.ReceivedDateTime || '';
218
+ const body = email.Body?.Content || email.body?.content || email.BodyPreview || email.bodyPreview || '';
219
+
220
+ content.innerHTML = `
221
+ <h2 class="detail-panel__subject">${escapeHtml(subject)}</h2>
222
+ <div class="detail-panel__meta">
223
+ <div class="detail-panel__meta-avatar">${getInitials(senderName)}</div>
224
+ <div class="detail-panel__meta-info">
225
+ <div class="detail-panel__meta-sender">${escapeHtml(senderName)}</div>
226
+ <div class="detail-panel__meta-email">${escapeHtml(senderEmail)}</div>
227
+ <div class="detail-panel__meta-date">${formatFullDate(dateStr)}</div>
228
+ </div>
229
+ </div>
230
+ <div class="detail-panel__body">${body}</div>
231
+ `;
232
+
233
+ // Mobile: show detail
234
+ const appBody = $('#appBody');
235
+ if (appBody) appBody.classList.add('app-body--detail-open');
236
+ };
237
+
238
+ // ===== FETCH INBOX =====
239
+ const fetchInbox = async () => {
240
+ setStatus('Loading inbox…', 'loading');
241
+ try {
242
+ const result = await execConnector(DATA_SOURCE, 'GetEmailsV3', {
243
+ folderPath: 'Inbox',
244
+ top: 25
245
+ });
246
+ emails = Array.isArray(result) ? result : [];
247
+ renderEmailList();
248
+ setStatus(`${emails.length} email${emails.length !== 1 ? 's' : ''} loaded`);
249
+ } catch (err) {
250
+ console.error('Failed to fetch inbox:', err);
251
+ setStatus(`Error: ${err.message}`, 'error');
252
+ const list = emailListEl();
253
+ const skeleton = loadingEl();
254
+ if (skeleton) skeleton.remove();
255
+ if (list) {
256
+ list.innerHTML = `<div style="padding:40px 20px;text-align:center;color:var(--accent);font-size:14px;">
257
+ Failed to load emails.<br><span style="color:var(--ink-muted);font-size:12px;">${escapeHtml(err.message)}</span>
258
+ </div>`;
259
+ }
260
+ }
261
+ };
262
+
263
+ // ===== SEND EMAIL =====
264
+ const sendEmail = async () => {
265
+ const toEl = $('#composeTo');
266
+ const subjectEl = $('#composeSubject');
267
+ const bodyEl = $('#composeBody');
268
+ const sendBtn = $('#btnSendEmail');
269
+
270
+ const to = toEl?.value?.trim();
271
+ const subject = subjectEl?.value?.trim();
272
+ const body = bodyEl?.value?.trim();
273
+
274
+ if (!to) {
275
+ toEl?.focus();
276
+ return;
277
+ }
278
+
279
+ sendBtn.disabled = true;
280
+ sendBtn.textContent = 'Sending…';
281
+ setStatus('Sending email…', 'loading');
282
+
283
+ try {
284
+ await execConnector(DATA_SOURCE, 'SendEmailV2', {
285
+ emailMessage: {
286
+ To: to,
287
+ Subject: subject || '(No subject)',
288
+ Body: `<p>${escapeHtml(body || '')}</p>`,
289
+ Importance: 'Normal'
290
+ }
291
+ });
292
+
293
+ setStatus('Email sent successfully');
294
+ closeCompose();
295
+
296
+ // Clear form
297
+ if (toEl) toEl.value = '';
298
+ if (subjectEl) subjectEl.value = '';
299
+ if (bodyEl) bodyEl.value = '';
300
+ } catch (err) {
301
+ console.error('Failed to send email:', err);
302
+ setStatus(`Send failed: ${err.message}`, 'error');
303
+ } finally {
304
+ sendBtn.disabled = false;
305
+ sendBtn.textContent = 'Send ➤';
306
+ }
307
+ };
308
+
309
+ // ===== COMPOSE MODAL =====
310
+ const openCompose = () => {
311
+ const overlay = composeOverlay();
312
+ if (overlay) overlay.classList.add('modal-overlay--visible');
313
+ setTimeout(() => $('#composeTo')?.focus(), 200);
314
+ };
315
+
316
+ const closeCompose = () => {
317
+ const overlay = composeOverlay();
318
+ if (overlay) overlay.classList.remove('modal-overlay--visible');
319
+ };
320
+
321
+ // ===== EVENT BINDINGS =====
322
+ const bindEvents = () => {
323
+ $('#btnCompose')?.addEventListener('click', openCompose);
324
+ $('#btnCloseCompose')?.addEventListener('click', closeCompose);
325
+ $('#btnDiscardCompose')?.addEventListener('click', closeCompose);
326
+ $('#btnSendEmail')?.addEventListener('click', sendEmail);
327
+ $('#btnRefresh')?.addEventListener('click', fetchInbox);
328
+
329
+ // Close modal on overlay click
330
+ composeOverlay()?.addEventListener('click', (e) => {
331
+ if (e.target === composeOverlay()) closeCompose();
332
+ });
333
+
334
+ // Escape key closes modal
335
+ document.addEventListener('keydown', (e) => {
336
+ if (e.key === 'Escape') closeCompose();
337
+ });
338
+ };
339
+
340
+ // ===== BOOT =====
341
+ async function boot() {
342
+ bindEvents();
343
+ await fetchInbox();
344
+ }
345
+
346
+ boot();