@zeyos/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +458 -0
  4. package/agents/README.md +66 -0
  5. package/agents/shared/business-app-benchmarks.md +111 -0
  6. package/agents/shared/zeyos-entity-map.md +142 -0
  7. package/agents/shared/zeyos-entity-reference.md +570 -0
  8. package/agents/shared/zeyos-query-patterns.md +89 -0
  9. package/agents/zeyos-account-intelligence/SKILL.md +34 -0
  10. package/agents/zeyos-account-intelligence/agents/openai.yaml +4 -0
  11. package/agents/zeyos-account-intelligence/references/workflows.md +84 -0
  12. package/agents/zeyos-billing-insights/SKILL.md +41 -0
  13. package/agents/zeyos-billing-insights/agents/openai.yaml +4 -0
  14. package/agents/zeyos-billing-insights/references/workflows.md +106 -0
  15. package/agents/zeyos-campaign-and-outreach/SKILL.md +44 -0
  16. package/agents/zeyos-campaign-and-outreach/agents/openai.yaml +4 -0
  17. package/agents/zeyos-campaign-and-outreach/references/workflows.md +100 -0
  18. package/agents/zeyos-collaboration-and-activity/SKILL.md +37 -0
  19. package/agents/zeyos-collaboration-and-activity/agents/openai.yaml +4 -0
  20. package/agents/zeyos-collaboration-and-activity/references/workflows.md +104 -0
  21. package/agents/zeyos-collections-and-dunning/SKILL.md +46 -0
  22. package/agents/zeyos-collections-and-dunning/agents/openai.yaml +4 -0
  23. package/agents/zeyos-collections-and-dunning/references/workflows.md +132 -0
  24. package/agents/zeyos-commerce-and-inventory/SKILL.md +38 -0
  25. package/agents/zeyos-commerce-and-inventory/agents/openai.yaml +4 -0
  26. package/agents/zeyos-commerce-and-inventory/references/workflows.md +101 -0
  27. package/agents/zeyos-mail-operations/SKILL.md +35 -0
  28. package/agents/zeyos-mail-operations/agents/openai.yaml +4 -0
  29. package/agents/zeyos-mail-operations/references/workflows.md +110 -0
  30. package/agents/zeyos-notes-and-sops/SKILL.md +31 -0
  31. package/agents/zeyos-notes-and-sops/agents/openai.yaml +4 -0
  32. package/agents/zeyos-notes-and-sops/references/workflows.md +85 -0
  33. package/agents/zeyos-platform-and-schema/SKILL.md +37 -0
  34. package/agents/zeyos-platform-and-schema/agents/openai.yaml +4 -0
  35. package/agents/zeyos-platform-and-schema/references/workflows.md +97 -0
  36. package/agents/zeyos-work-management/SKILL.md +45 -0
  37. package/agents/zeyos-work-management/agents/openai.yaml +4 -0
  38. package/agents/zeyos-work-management/references/workflows.md +148 -0
  39. package/docs/01-api-reference/01-data-retrieval.md +601 -0
  40. package/docs/01-api-reference/02-authentication.md +288 -0
  41. package/docs/01-api-reference/03-resources.md +270 -0
  42. package/docs/01-api-reference/04-schema.md +539 -0
  43. package/docs/01-api-reference/_category_.json +9 -0
  44. package/docs/02-javascript-client/01-getting-started.md +146 -0
  45. package/docs/02-javascript-client/02-authentication.md +287 -0
  46. package/docs/02-javascript-client/03-making-requests.md +572 -0
  47. package/docs/02-javascript-client/04-practical-guide.md +348 -0
  48. package/docs/02-javascript-client/_category_.json +9 -0
  49. package/docs/03-cli/01-getting-started.md +219 -0
  50. package/docs/03-cli/02-commands.md +407 -0
  51. package/docs/03-cli/03-configuration.md +220 -0
  52. package/docs/03-cli/_category_.json +9 -0
  53. package/docs/04-agent-workflows/00-coding-agents.md +35 -0
  54. package/docs/04-agent-workflows/01-agent-quickstart.md +147 -0
  55. package/docs/04-agent-workflows/02-agent-recipes.md +109 -0
  56. package/docs/04-agent-workflows/03-cli-coverage-and-escalation.md +65 -0
  57. package/docs/04-agent-workflows/_category_.json +9 -0
  58. package/docs/04-sample-apps/01-kanban.md +89 -0
  59. package/docs/04-sample-apps/02-crm.md +81 -0
  60. package/docs/04-sample-apps/03-dashboard.md +80 -0
  61. package/docs/04-sample-apps/_category_.json +9 -0
  62. package/docs/05-tutorials/00-application-developers.md +43 -0
  63. package/docs/05-tutorials/01-integration-architecture.md +60 -0
  64. package/docs/05-tutorials/02-build-your-own-zeyos-frontend.md +517 -0
  65. package/docs/05-tutorials/03-server-side-integrations.md +185 -0
  66. package/docs/05-tutorials/_category_.json +9 -0
  67. package/docs/intro.md +197 -0
  68. package/openapi/api.json +24308 -0
  69. package/openapi/auth.json +415 -0
  70. package/openapi/dbref.json +56223 -0
  71. package/openapi/oauth2.json +781 -0
  72. package/openapi/sdk.json +949 -0
  73. package/openapi/views.txt +642 -0
  74. package/package.json +49 -0
  75. package/samples/crm/README.md +28 -0
  76. package/samples/crm/index.html +327 -0
  77. package/samples/crm/js/api.js +208 -0
  78. package/samples/crm/js/auth.js +61 -0
  79. package/samples/crm/js/main.js +545 -0
  80. package/samples/crm/js/state.js +90 -0
  81. package/samples/crm/js/ui.js +51 -0
  82. package/samples/dashboard/README.md +28 -0
  83. package/samples/dashboard/index.html +280 -0
  84. package/samples/dashboard/js/api.js +197 -0
  85. package/samples/dashboard/js/auth.js +59 -0
  86. package/samples/dashboard/js/main.js +382 -0
  87. package/samples/dashboard/js/state.js +81 -0
  88. package/samples/dashboard/js/ui.js +48 -0
  89. package/samples/kanban/README.md +28 -0
  90. package/samples/kanban/index.html +263 -0
  91. package/samples/kanban/js/api.js +152 -0
  92. package/samples/kanban/js/auth.js +59 -0
  93. package/samples/kanban/js/constants.js +40 -0
  94. package/samples/kanban/js/kanban.js +246 -0
  95. package/samples/kanban/js/main.js +362 -0
  96. package/samples/kanban/js/modals.js +474 -0
  97. package/samples/kanban/js/settings.js +82 -0
  98. package/samples/kanban/js/state.js +118 -0
  99. package/samples/kanban/js/ui.js +49 -0
  100. package/scripts/generate-client.mjs +344 -0
  101. package/src/generated/operations.js +9772 -0
  102. package/src/generated/schema.js +8982 -0
  103. package/src/index.js +85 -0
  104. package/src/runtime/client.js +1208 -0
  105. package/src/runtime/error.js +29 -0
  106. package/src/runtime/http.js +174 -0
  107. package/src/runtime/request-shape.js +35 -0
  108. package/src/runtime/schema.js +206 -0
  109. package/src/runtime/suggest.js +74 -0
  110. package/src/runtime/token-store.js +105 -0
@@ -0,0 +1,545 @@
1
+ /**
2
+ * CRM Account List -- Application entry point.
3
+ *
4
+ * Bootstraps the client, handles auth, wires up all UI events,
5
+ * and orchestrates data loading / table rendering.
6
+ *
7
+ * Authentication: reads config from <body> data attributes + localStorage.
8
+ * 1. If tokens are available -> token mode
9
+ * 2. If URL only -> try session detection via /oauth2/v1/userinfo
10
+ * 3. Otherwise -> connection screen with troubleshooting
11
+ *
12
+ * Exposes a global ZeyOS console API for debugging / configuration.
13
+ */
14
+ import {
15
+ initTokenClient, initSessionClient,
16
+ fetchAccounts, getAccount, createAccount, updateAccount, deleteAccount,
17
+ } from './api.js';
18
+ import { trySessionAuth, logout } from './auth.js';
19
+ import { runtime, resolveConfig, saveUrl, saveTokens, clearTokens, clearUrl } from './state.js';
20
+ import { showToast, showLoading, hideLoading } from './ui.js';
21
+
22
+ // ── Account Type Labels & Colors ────────────────────────────────────────────
23
+ // 0 = Prospect, 1 = Customer, 2 = Supplier, 3 = Cust. & Suppl., 4 = Competitor, 5 = Employee
24
+
25
+ const ACCOUNT_TYPES = {
26
+ 0: { label: 'Prospect', bg: 'bg-amber-100', text: 'text-amber-700' },
27
+ 1: { label: 'Customer', bg: 'bg-emerald-100', text: 'text-emerald-700' },
28
+ 2: { label: 'Supplier', bg: 'bg-purple-100', text: 'text-purple-700' },
29
+ 3: { label: 'Cust. & Suppl.', bg: 'bg-blue-100', text: 'text-blue-700' },
30
+ 4: { label: 'Competitor', bg: 'bg-red-100', text: 'text-red-700' },
31
+ 5: { label: 'Employee', bg: 'bg-slate-100', text: 'text-slate-700' },
32
+ };
33
+
34
+ // ── Debounce helper ─────────────────────────────────────────────────────────
35
+
36
+ let _searchTimer = null;
37
+ function debounce(fn, ms = 350) {
38
+ return (...args) => {
39
+ clearTimeout(_searchTimer);
40
+ _searchTimer = setTimeout(() => fn(...args), ms);
41
+ };
42
+ }
43
+
44
+ // ── HTML escaping ───────────────────────────────────────────────────────────
45
+
46
+ function esc(str) {
47
+ return String(str ?? '')
48
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;')
49
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
50
+ }
51
+
52
+ // ── Format Unix timestamp (seconds) to locale date ──────────────────────────
53
+
54
+ function formatDate(ts) {
55
+ if (!ts) return '\u2014';
56
+ // ZeyOS timestamps are in SECONDS (not ms)
57
+ const d = new Date(ts * 1000);
58
+ return d.toLocaleDateString(undefined, {
59
+ year: 'numeric', month: 'short', day: 'numeric',
60
+ hour: '2-digit', minute: '2-digit',
61
+ });
62
+ }
63
+
64
+ // ── Boot ─────────────────────────────────────────────────────────────────────
65
+
66
+ document.addEventListener('DOMContentLoaded', async () => {
67
+ const config = resolveConfig();
68
+
69
+ // 1. No URL configured -> show connection screen immediately
70
+ if (!config.url) {
71
+ _showConnectionScreen('No ZeyOS URL configured.');
72
+ return;
73
+ }
74
+
75
+ runtime.url = config.url;
76
+
77
+ // 2. Tokens available -> initialize in token mode
78
+ if (config.accessToken) {
79
+ saveTokens({
80
+ accessToken: config.accessToken,
81
+ refreshToken: config.refreshToken ?? null,
82
+ });
83
+ initTokenClient(config.url);
84
+ runtime.authMode = 'token';
85
+ await _bootApp();
86
+ return;
87
+ }
88
+
89
+ // 3. No tokens -> try session detection
90
+ showLoading();
91
+ const userInfo = await trySessionAuth(config.url);
92
+ hideLoading();
93
+
94
+ if (userInfo) {
95
+ initSessionClient(config.url);
96
+ runtime.authMode = 'session';
97
+ await _bootApp();
98
+ return;
99
+ }
100
+
101
+ // 4. Nothing works -> connection screen
102
+ _showConnectionScreen('Could not connect. Set a token or log into ZeyOS first.');
103
+ });
104
+
105
+ // ── Connection Screen ────────────────────────────────────────────────────────
106
+
107
+ function _showConnectionScreen(message) {
108
+ document.getElementById('connection-screen')?.classList.remove('hidden');
109
+ document.getElementById('app-shell')?.classList.add('hidden');
110
+
111
+ const msgEl = document.getElementById('connection-message');
112
+ if (msgEl) msgEl.textContent = message;
113
+ }
114
+
115
+ // ── Main App Boot ────────────────────────────────────────────────────────────
116
+
117
+ async function _bootApp() {
118
+ document.getElementById('connection-screen')?.classList.add('hidden');
119
+ document.getElementById('app-shell')?.classList.remove('hidden');
120
+
121
+ _wireEvents();
122
+
123
+ showLoading();
124
+ try {
125
+ await _loadAccounts();
126
+ _renderTable();
127
+ } catch (err) {
128
+ if (err?.status === 401) {
129
+ showToast('Session expired. Please log in again.', 'error');
130
+ setTimeout(async () => { await logout(); location.reload(); }, 2000);
131
+ return;
132
+ }
133
+ showToast(`Failed to load data: ${err.message}`, 'error');
134
+ } finally {
135
+ hideLoading();
136
+ }
137
+ }
138
+
139
+ // ── Data Loading ─────────────────────────────────────────────────────────────
140
+
141
+ async function _loadAccounts() {
142
+ const offset = (runtime.page - 1) * runtime.pageSize;
143
+
144
+ const records = await fetchAccounts({
145
+ search: runtime.search || undefined,
146
+ sortField: runtime.sort.field,
147
+ sortDir: runtime.sort.dir,
148
+ limit: runtime.pageSize + 1,
149
+ offset,
150
+ });
151
+
152
+ runtime.hasNextPage = records.length > runtime.pageSize;
153
+ runtime.accounts = records.slice(0, runtime.pageSize);
154
+ }
155
+
156
+ async function _refresh() {
157
+ showLoading();
158
+ try {
159
+ await _loadAccounts();
160
+ _renderTable();
161
+ } catch (err) {
162
+ showToast(`Refresh failed: ${err.message}`, 'error');
163
+ } finally {
164
+ hideLoading();
165
+ }
166
+ }
167
+
168
+ // ── Table Rendering ──────────────────────────────────────────────────────────
169
+
170
+ function _renderTable() {
171
+ const tbody = document.getElementById('accounts-tbody');
172
+ const empty = document.getElementById('empty-state');
173
+ const pagBar = document.getElementById('pagination-bar');
174
+ const accounts = runtime.accounts;
175
+
176
+ if (!tbody) return;
177
+
178
+ if (accounts.length === 0) {
179
+ tbody.innerHTML = '';
180
+ empty?.classList.remove('hidden');
181
+ } else {
182
+ empty?.classList.add('hidden');
183
+ tbody.innerHTML = accounts.map(a => _accountRow(a)).join('');
184
+ }
185
+
186
+ _updateSortIndicators();
187
+
188
+ _updatePagination();
189
+ }
190
+
191
+ function _accountRow(a) {
192
+ const typeInfo = ACCOUNT_TYPES[a.Type] ?? { label: 'Unknown', bg: 'bg-slate-100', text: 'text-slate-600' };
193
+ const name = [a.FirstName, a.Name].filter(Boolean).join(' ') || '\u2014';
194
+
195
+ return `
196
+ <tr data-id="${esc(a.Id)}" class="transition-colors">
197
+ <td class="px-4 py-3 text-slate-500 font-mono text-xs">${esc(a.AccountNum) || '\u2014'}</td>
198
+ <td class="px-4 py-3 font-medium text-slate-800">${esc(name)}</td>
199
+ <td class="px-4 py-3 text-slate-600">${esc(a.Email) || '\u2014'}</td>
200
+ <td class="px-4 py-3 text-slate-600">${esc(a.Phone) || '\u2014'}</td>
201
+ <td class="px-4 py-3 text-slate-600">${esc(a.City) || '\u2014'}</td>
202
+ <td class="px-4 py-3 text-slate-600">${esc(a.AssignedUser) || '\u2014'}</td>
203
+ <td class="px-4 py-3">
204
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${typeInfo.bg} ${typeInfo.text}">
205
+ ${typeInfo.label}
206
+ </span>
207
+ </td>
208
+ <td class="px-4 py-3 text-slate-500 text-xs whitespace-nowrap">${formatDate(a.LastModified)}</td>
209
+ </tr>
210
+ `;
211
+ }
212
+
213
+ // ── Sort Indicators ──────────────────────────────────────────────────────────
214
+
215
+ function _updateSortIndicators() {
216
+ document.querySelectorAll('.sortable-col').forEach(th => {
217
+ th.classList.remove('sort-asc', 'sort-desc', 'text-blue-600');
218
+ if (th.dataset.sort === runtime.sort.field) {
219
+ th.classList.add(runtime.sort.dir === 'asc' ? 'sort-asc' : 'sort-desc');
220
+ th.classList.add('text-blue-600');
221
+ }
222
+ });
223
+ }
224
+
225
+ // ── Pagination ───────────────────────────────────────────────────────────────
226
+
227
+ function _updatePagination() {
228
+ const count = runtime.accounts.length;
229
+ const offset = (runtime.page - 1) * runtime.pageSize;
230
+ const infoEl = document.getElementById('pagination-info');
231
+ const pageEl = document.getElementById('page-indicator');
232
+ const prevBtn = document.getElementById('btn-prev-page');
233
+ const nextBtn = document.getElementById('btn-next-page');
234
+
235
+ if (infoEl) {
236
+ if (count === 0) {
237
+ infoEl.textContent = 'No accounts found';
238
+ } else {
239
+ infoEl.textContent = `Showing ${offset + 1}\u2013${offset + count} accounts`;
240
+ }
241
+ }
242
+
243
+ if (pageEl) {
244
+ pageEl.textContent = `Page ${runtime.page}`;
245
+ }
246
+
247
+ if (prevBtn) prevBtn.disabled = (runtime.page <= 1);
248
+
249
+ if (nextBtn) nextBtn.disabled = !runtime.hasNextPage;
250
+ }
251
+
252
+ // ── Event Wiring ─────────────────────────────────────────────────────────────
253
+
254
+ function _wireEvents() {
255
+ // -- Search input (debounced) --
256
+ const searchInput = document.getElementById('search-input');
257
+ if (searchInput) {
258
+ searchInput.addEventListener('input', debounce(async () => {
259
+ runtime.search = searchInput.value;
260
+ runtime.page = 1; // Reset to first page on new search
261
+ await _refresh();
262
+ }));
263
+ }
264
+
265
+ // -- Sortable column headers --
266
+ document.querySelectorAll('.sortable-col').forEach(th => {
267
+ th.addEventListener('click', async () => {
268
+ const field = th.dataset.sort;
269
+ if (!field) return;
270
+
271
+ // Toggle direction if same column, otherwise default to ascending
272
+ if (runtime.sort.field === field) {
273
+ runtime.sort.dir = runtime.sort.dir === 'asc' ? 'desc' : 'asc';
274
+ } else {
275
+ runtime.sort.field = field;
276
+ runtime.sort.dir = 'asc';
277
+ }
278
+
279
+ runtime.page = 1; // Reset to first page on sort change
280
+ await _refresh();
281
+ });
282
+ });
283
+
284
+ // -- Pagination --
285
+ document.getElementById('btn-prev-page')?.addEventListener('click', async () => {
286
+ if (runtime.page > 1) {
287
+ runtime.page--;
288
+ await _refresh();
289
+ }
290
+ });
291
+
292
+ document.getElementById('btn-next-page')?.addEventListener('click', async () => {
293
+ if (runtime.hasNextPage) {
294
+ runtime.page++;
295
+ await _refresh();
296
+ }
297
+ });
298
+
299
+ // -- Reload button --
300
+ document.getElementById('btn-reload')?.addEventListener('click', async () => {
301
+ await _refresh();
302
+ });
303
+
304
+ // -- New Account button --
305
+ document.getElementById('btn-new-account')?.addEventListener('click', () => {
306
+ _openModal('create');
307
+ });
308
+
309
+ // -- Table row click -> open edit modal --
310
+ document.getElementById('accounts-tbody')?.addEventListener('click', e => {
311
+ const row = e.target.closest('tr[data-id]');
312
+ if (!row) return;
313
+ const id = row.dataset.id;
314
+ if (id) _openModal('edit', id);
315
+ });
316
+
317
+ // -- Logout --
318
+ document.getElementById('btn-logout')?.addEventListener('click', async () => {
319
+ if (!confirm('Log out of ZeyOS CRM?')) return;
320
+ await logout();
321
+ clearUrl();
322
+ location.reload();
323
+ });
324
+
325
+ // -- Modal controls --
326
+ document.getElementById('modal-close')?.addEventListener('click', _closeModal);
327
+ document.getElementById('btn-cancel')?.addEventListener('click', _closeModal);
328
+ document.getElementById('account-form')?.addEventListener('submit', _handleFormSubmit);
329
+ document.getElementById('btn-delete-account')?.addEventListener('click', _handleDelete);
330
+
331
+ // Close modal on backdrop click
332
+ const dialog = document.getElementById('account-modal');
333
+ dialog?.addEventListener('click', e => {
334
+ if (e.target === dialog) _closeModal();
335
+ });
336
+ }
337
+
338
+ // ── Modal Logic ──────────────────────────────────────────────────────────────
339
+
340
+ /**
341
+ * Open the account modal in 'create' or 'edit' mode.
342
+ * @param {'create'|'edit'} mode
343
+ * @param {string|number} [id] - Account ID for edit mode
344
+ */
345
+ async function _openModal(mode, id) {
346
+ const dialog = document.getElementById('account-modal');
347
+ const title = document.getElementById('modal-title');
348
+ const formId = document.getElementById('form-id');
349
+ const firstname = document.getElementById('form-firstname');
350
+ const lastname = document.getElementById('form-lastname');
351
+ const type = document.getElementById('form-type');
352
+ const description = document.getElementById('form-description');
353
+ const contactSection = document.getElementById('contact-section');
354
+ const deleteBtn = document.getElementById('btn-delete-account');
355
+ const contactEmail = document.getElementById('contact-email');
356
+ const contactPhone = document.getElementById('contact-phone');
357
+ const contactCity = document.getElementById('contact-city');
358
+
359
+ if (!dialog) return;
360
+
361
+ if (mode === 'create') {
362
+ title.textContent = 'New Account';
363
+ formId.value = '';
364
+ firstname.value = '';
365
+ lastname.value = '';
366
+ type.value = '0';
367
+ description.value = '';
368
+
369
+ contactSection?.classList.add('hidden');
370
+ deleteBtn?.classList.add('hidden');
371
+
372
+ dialog.showModal();
373
+ } else {
374
+ title.textContent = 'Edit Account';
375
+ deleteBtn?.classList.remove('hidden');
376
+ contactSection?.classList.remove('hidden');
377
+
378
+ showLoading();
379
+ try {
380
+ const account = await getAccount(id);
381
+
382
+ formId.value = account.ID ?? id;
383
+ firstname.value = account.firstname ?? '';
384
+ lastname.value = account.lastname ?? '';
385
+ type.value = String(account.type ?? 0);
386
+ description.value = account.description ?? '';
387
+
388
+ // Also look up the row in runtime.accounts for the joined contact fields
389
+ const row = runtime.accounts.find(a => String(a.Id) === String(id));
390
+
391
+ if (contactEmail) contactEmail.textContent = row?.Email || account.email || '\u2014';
392
+ if (contactPhone) contactPhone.textContent = row?.Phone || account.phone || '\u2014';
393
+ if (contactCity) contactCity.textContent = row?.City || account.city || '\u2014';
394
+
395
+ dialog.showModal();
396
+ } catch (err) {
397
+ showToast(`Failed to load account: ${err.message}`, 'error');
398
+ } finally {
399
+ hideLoading();
400
+ }
401
+ }
402
+ }
403
+
404
+ function _closeModal() {
405
+ document.getElementById('account-modal')?.close();
406
+ }
407
+
408
+ /**
409
+ * Handle form submission -- create or update depending on form-id value.
410
+ */
411
+ async function _handleFormSubmit(e) {
412
+ e.preventDefault();
413
+
414
+ const id = document.getElementById('form-id')?.value;
415
+ const data = {
416
+ firstname: document.getElementById('form-firstname')?.value?.trim() || null,
417
+ lastname: document.getElementById('form-lastname')?.value?.trim() || '',
418
+ type: Number(document.getElementById('form-type')?.value ?? 0),
419
+ description: document.getElementById('form-description')?.value?.trim() || null,
420
+ };
421
+
422
+ if (!data.lastname) {
423
+ showToast('Last name / company is required.', 'error');
424
+ return;
425
+ }
426
+
427
+ showLoading();
428
+ try {
429
+ if (id) {
430
+ await updateAccount(id, data);
431
+ showToast('Account updated.', 'success');
432
+ } else {
433
+ await createAccount(data);
434
+ showToast('Account created.', 'success');
435
+ }
436
+
437
+ _closeModal();
438
+ await _refresh();
439
+ } catch (err) {
440
+ showToast(`Save failed: ${err.message}`, 'error');
441
+ } finally {
442
+ hideLoading();
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Handle the delete button inside the edit modal.
448
+ */
449
+ async function _handleDelete() {
450
+ const id = document.getElementById('form-id')?.value;
451
+ if (!id) return;
452
+
453
+ const name = document.getElementById('form-lastname')?.value || 'this account';
454
+ if (!confirm(`Delete "${name}"? This cannot be undone.`)) return;
455
+
456
+ showLoading();
457
+ try {
458
+ await deleteAccount(id);
459
+ showToast('Account deleted.', 'success');
460
+ _closeModal();
461
+ await _refresh();
462
+ } catch (err) {
463
+ showToast(`Delete failed: ${err.message}`, 'error');
464
+ } finally {
465
+ hideLoading();
466
+ }
467
+ }
468
+
469
+ // ── Console API ──────────────────────────────────────────────────────────────
470
+ // Provides ZeyOS.setUrl(), ZeyOS.setToken(), ZeyOS.status(), etc.
471
+ // Values set via the console are persisted to localStorage and override
472
+ // <body> data attributes on next page load.
473
+
474
+ globalThis.ZeyOS = {
475
+ /**
476
+ * Set the ZeyOS instance URL.
477
+ * @param {string} url - e.g. 'https://cloud.zeyos.com/demo/'
478
+ */
479
+ setUrl(url) {
480
+ if (!url || typeof url !== 'string') {
481
+ console.error('[ZeyOS] Usage: ZeyOS.setUrl("https://cloud.zeyos.com/demo/")');
482
+ return;
483
+ }
484
+ saveUrl(url.trim());
485
+ console.log(`%c[ZeyOS]%c URL set to: ${url}`, 'color:#2563eb;font-weight:bold', '');
486
+ console.log('%c[ZeyOS]%c Call ZeyOS.reconnect() to apply.', 'color:#2563eb;font-weight:bold', '');
487
+ },
488
+
489
+ /**
490
+ * Set access (and optionally refresh) token.
491
+ * @param {string} accessToken
492
+ * @param {string} [refreshToken]
493
+ */
494
+ setToken(accessToken, refreshToken) {
495
+ if (!accessToken || typeof accessToken !== 'string') {
496
+ console.error('[ZeyOS] Usage: ZeyOS.setToken("access-token", "optional-refresh-token")');
497
+ return;
498
+ }
499
+ saveTokens({
500
+ accessToken: accessToken.trim(),
501
+ refreshToken: refreshToken?.trim() ?? null,
502
+ });
503
+ console.log('%c[ZeyOS]%c Token saved.', 'color:#2563eb;font-weight:bold', '');
504
+ console.log('%c[ZeyOS]%c Call ZeyOS.reconnect() to apply.', 'color:#2563eb;font-weight:bold', '');
505
+ },
506
+
507
+ /**
508
+ * Print the current connection status to the console.
509
+ */
510
+ status() {
511
+ const config = resolveConfig();
512
+ const lines = [
513
+ '',
514
+ ` URL: ${config.url ?? '(not set)'}`,
515
+ ` Access Token: ${config.accessToken ? config.accessToken.slice(0, 16) + '...' : '(not set)'}`,
516
+ ` Refresh Token: ${config.refreshToken ? 'yes' : 'no'}`,
517
+ ` Auth Mode: ${runtime.authMode ?? '(not connected)'}`,
518
+ ` Page: ${runtime.page}`,
519
+ ` Page Size: ${runtime.pageSize}`,
520
+ ` Search: ${runtime.search || '(none)'}`,
521
+ ` Sort: ${runtime.sort.field} ${runtime.sort.dir}`,
522
+ ` Loaded: ${runtime.accounts.length} accounts`,
523
+ '',
524
+ ];
525
+ console.log(`%c[ZeyOS] Status%c\n${lines.join('\n')}`, 'color:#2563eb;font-weight:bold', 'color:inherit');
526
+ },
527
+
528
+ /**
529
+ * Clear all stored config (URL + tokens) and reload.
530
+ */
531
+ logout() {
532
+ clearTokens();
533
+ clearUrl();
534
+ console.log('%c[ZeyOS]%c Config cleared. Reloading...', 'color:#2563eb;font-weight:bold', '');
535
+ location.reload();
536
+ },
537
+
538
+ /**
539
+ * Reload the page to re-run the boot sequence with current config.
540
+ */
541
+ reconnect() {
542
+ console.log('%c[ZeyOS]%c Reconnecting...', 'color:#2563eb;font-weight:bold', '');
543
+ location.reload();
544
+ },
545
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Application state -- persistence via localStorage, in-memory for runtime data.
3
+ * Config resolution merges <body> data attributes with localStorage overrides.
4
+ *
5
+ * This follows the same pattern as the kanban sample app, using a separate
6
+ * localStorage namespace (zeyos_crm_*) to avoid collisions.
7
+ */
8
+
9
+ const KEYS = {
10
+ URL: 'zeyos_crm_url',
11
+ TOKENS: 'zeyos_crm_tokens',
12
+ };
13
+
14
+ // ── helpers ────────────────────────────────────────────────────────────────
15
+
16
+ function readJson(key, fallback) {
17
+ try {
18
+ const raw = localStorage.getItem(key);
19
+ return raw != null ? JSON.parse(raw) : fallback;
20
+ } catch {
21
+ return fallback;
22
+ }
23
+ }
24
+
25
+ function writeJson(key, value) {
26
+ try {
27
+ localStorage.setItem(key, JSON.stringify(value));
28
+ } catch {
29
+ // storage full or unavailable -- silently ignore
30
+ }
31
+ }
32
+
33
+ // ── URL ────────────────────────────────────────────────────────────────────
34
+
35
+ export function loadUrl() { return localStorage.getItem(KEYS.URL) ?? null; }
36
+ export function saveUrl(url) { localStorage.setItem(KEYS.URL, url); }
37
+ export function clearUrl() { localStorage.removeItem(KEYS.URL); }
38
+
39
+ // ── Tokens ─────────────────────────────────────────────────────────────────
40
+
41
+ export function loadTokens() {
42
+ return readJson(KEYS.TOKENS, null);
43
+ }
44
+
45
+ export function saveTokens(tokens) {
46
+ writeJson(KEYS.TOKENS, tokens);
47
+ }
48
+
49
+ export function clearTokens() {
50
+ localStorage.removeItem(KEYS.TOKENS);
51
+ }
52
+
53
+ // ── Config Resolution ──────────────────────────────────────────────────────
54
+
55
+ /**
56
+ * Resolve effective config by merging <body> data attributes with localStorage.
57
+ * localStorage values (set via ZeyOS console API) override body attributes.
58
+ * Returns { url, accessToken, refreshToken }.
59
+ */
60
+ export function resolveConfig() {
61
+ const body = document.body;
62
+
63
+ // Body attributes (defaults)
64
+ const bodyUrl = body?.dataset.zeyosUrl?.trim() || null;
65
+ const bodyAccess = body?.dataset.zeyosAccesstoken?.trim() || null;
66
+ const bodyRefresh = body?.dataset.zeyosRefreshtoken?.trim() || null;
67
+
68
+ // localStorage overrides
69
+ const storedUrl = loadUrl();
70
+ const storedTokens = loadTokens();
71
+
72
+ return {
73
+ url: storedUrl || bodyUrl,
74
+ accessToken: storedTokens?.accessToken || bodyAccess,
75
+ refreshToken: storedTokens?.refreshToken || bodyRefresh,
76
+ };
77
+ }
78
+
79
+ // ── In-memory runtime state (not persisted) ────────────────────────────────
80
+
81
+ export const runtime = {
82
+ url: null, // resolved ZeyOS instance URL
83
+ authMode: null, // 'token' | 'session' | null
84
+ accounts: [], // current page of account records
85
+ hasNextPage: false, // true when the next page has at least one record
86
+ page: 1, // current page number (1-based)
87
+ pageSize: 25, // records per page
88
+ search: '', // current search query
89
+ sort: { field: 'LastModified', dir: 'desc' }, // active sort column + direction
90
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shared UI utilities: toast notifications + loading overlay.
3
+ * Same pattern as the kanban sample -- extracted to avoid circular imports.
4
+ */
5
+
6
+ // ── Toast Notifications ────────────────────────────────────────────────────
7
+
8
+ /**
9
+ * Show a brief toast notification in the bottom-right corner.
10
+ * @param {string} message
11
+ * @param {'success'|'error'|'info'} type
12
+ */
13
+ export function showToast(message, type = 'info') {
14
+ const container = document.getElementById('toast-container');
15
+ if (!container) return;
16
+
17
+ const colors = {
18
+ success: 'bg-emerald-600',
19
+ error: 'bg-red-600',
20
+ info: 'bg-slate-700',
21
+ };
22
+
23
+ const toast = document.createElement('div');
24
+ toast.className =
25
+ `pointer-events-auto flex items-center gap-3 px-4 py-3 rounded-xl text-white text-sm shadow-xl ` +
26
+ `${colors[type] ?? colors.info} translate-y-2 opacity-0 transition-all duration-200`;
27
+ toast.textContent = message;
28
+
29
+ container.appendChild(toast);
30
+
31
+ // Trigger CSS transition on next frame
32
+ requestAnimationFrame(() => {
33
+ requestAnimationFrame(() => toast.classList.remove('translate-y-2', 'opacity-0'));
34
+ });
35
+
36
+ const duration = type === 'error' ? 5000 : 3000;
37
+ setTimeout(() => {
38
+ toast.classList.add('opacity-0', 'translate-y-2');
39
+ toast.addEventListener('transitionend', () => toast.remove(), { once: true });
40
+ }, duration);
41
+ }
42
+
43
+ // ── Loading Overlay ────────────────────────────────────────────────────────
44
+
45
+ export function showLoading() {
46
+ document.getElementById('loading-overlay')?.classList.remove('hidden');
47
+ }
48
+
49
+ export function hideLoading() {
50
+ document.getElementById('loading-overlay')?.classList.add('hidden');
51
+ }