@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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +458 -0
- package/agents/README.md +66 -0
- package/agents/shared/business-app-benchmarks.md +111 -0
- package/agents/shared/zeyos-entity-map.md +142 -0
- package/agents/shared/zeyos-entity-reference.md +570 -0
- package/agents/shared/zeyos-query-patterns.md +89 -0
- package/agents/zeyos-account-intelligence/SKILL.md +34 -0
- package/agents/zeyos-account-intelligence/agents/openai.yaml +4 -0
- package/agents/zeyos-account-intelligence/references/workflows.md +84 -0
- package/agents/zeyos-billing-insights/SKILL.md +41 -0
- package/agents/zeyos-billing-insights/agents/openai.yaml +4 -0
- package/agents/zeyos-billing-insights/references/workflows.md +106 -0
- package/agents/zeyos-campaign-and-outreach/SKILL.md +44 -0
- package/agents/zeyos-campaign-and-outreach/agents/openai.yaml +4 -0
- package/agents/zeyos-campaign-and-outreach/references/workflows.md +100 -0
- package/agents/zeyos-collaboration-and-activity/SKILL.md +37 -0
- package/agents/zeyos-collaboration-and-activity/agents/openai.yaml +4 -0
- package/agents/zeyos-collaboration-and-activity/references/workflows.md +104 -0
- package/agents/zeyos-collections-and-dunning/SKILL.md +46 -0
- package/agents/zeyos-collections-and-dunning/agents/openai.yaml +4 -0
- package/agents/zeyos-collections-and-dunning/references/workflows.md +132 -0
- package/agents/zeyos-commerce-and-inventory/SKILL.md +38 -0
- package/agents/zeyos-commerce-and-inventory/agents/openai.yaml +4 -0
- package/agents/zeyos-commerce-and-inventory/references/workflows.md +101 -0
- package/agents/zeyos-mail-operations/SKILL.md +35 -0
- package/agents/zeyos-mail-operations/agents/openai.yaml +4 -0
- package/agents/zeyos-mail-operations/references/workflows.md +110 -0
- package/agents/zeyos-notes-and-sops/SKILL.md +31 -0
- package/agents/zeyos-notes-and-sops/agents/openai.yaml +4 -0
- package/agents/zeyos-notes-and-sops/references/workflows.md +85 -0
- package/agents/zeyos-platform-and-schema/SKILL.md +37 -0
- package/agents/zeyos-platform-and-schema/agents/openai.yaml +4 -0
- package/agents/zeyos-platform-and-schema/references/workflows.md +97 -0
- package/agents/zeyos-work-management/SKILL.md +45 -0
- package/agents/zeyos-work-management/agents/openai.yaml +4 -0
- package/agents/zeyos-work-management/references/workflows.md +148 -0
- package/docs/01-api-reference/01-data-retrieval.md +601 -0
- package/docs/01-api-reference/02-authentication.md +288 -0
- package/docs/01-api-reference/03-resources.md +270 -0
- package/docs/01-api-reference/04-schema.md +539 -0
- package/docs/01-api-reference/_category_.json +9 -0
- package/docs/02-javascript-client/01-getting-started.md +146 -0
- package/docs/02-javascript-client/02-authentication.md +287 -0
- package/docs/02-javascript-client/03-making-requests.md +572 -0
- package/docs/02-javascript-client/04-practical-guide.md +348 -0
- package/docs/02-javascript-client/_category_.json +9 -0
- package/docs/03-cli/01-getting-started.md +219 -0
- package/docs/03-cli/02-commands.md +407 -0
- package/docs/03-cli/03-configuration.md +220 -0
- package/docs/03-cli/_category_.json +9 -0
- package/docs/04-agent-workflows/00-coding-agents.md +35 -0
- package/docs/04-agent-workflows/01-agent-quickstart.md +147 -0
- package/docs/04-agent-workflows/02-agent-recipes.md +109 -0
- package/docs/04-agent-workflows/03-cli-coverage-and-escalation.md +65 -0
- package/docs/04-agent-workflows/_category_.json +9 -0
- package/docs/04-sample-apps/01-kanban.md +89 -0
- package/docs/04-sample-apps/02-crm.md +81 -0
- package/docs/04-sample-apps/03-dashboard.md +80 -0
- package/docs/04-sample-apps/_category_.json +9 -0
- package/docs/05-tutorials/00-application-developers.md +43 -0
- package/docs/05-tutorials/01-integration-architecture.md +60 -0
- package/docs/05-tutorials/02-build-your-own-zeyos-frontend.md +517 -0
- package/docs/05-tutorials/03-server-side-integrations.md +185 -0
- package/docs/05-tutorials/_category_.json +9 -0
- package/docs/intro.md +197 -0
- package/openapi/api.json +24308 -0
- package/openapi/auth.json +415 -0
- package/openapi/dbref.json +56223 -0
- package/openapi/oauth2.json +781 -0
- package/openapi/sdk.json +949 -0
- package/openapi/views.txt +642 -0
- package/package.json +49 -0
- package/samples/crm/README.md +28 -0
- package/samples/crm/index.html +327 -0
- package/samples/crm/js/api.js +208 -0
- package/samples/crm/js/auth.js +61 -0
- package/samples/crm/js/main.js +545 -0
- package/samples/crm/js/state.js +90 -0
- package/samples/crm/js/ui.js +51 -0
- package/samples/dashboard/README.md +28 -0
- package/samples/dashboard/index.html +280 -0
- package/samples/dashboard/js/api.js +197 -0
- package/samples/dashboard/js/auth.js +59 -0
- package/samples/dashboard/js/main.js +382 -0
- package/samples/dashboard/js/state.js +81 -0
- package/samples/dashboard/js/ui.js +48 -0
- package/samples/kanban/README.md +28 -0
- package/samples/kanban/index.html +263 -0
- package/samples/kanban/js/api.js +152 -0
- package/samples/kanban/js/auth.js +59 -0
- package/samples/kanban/js/constants.js +40 -0
- package/samples/kanban/js/kanban.js +246 -0
- package/samples/kanban/js/main.js +362 -0
- package/samples/kanban/js/modals.js +474 -0
- package/samples/kanban/js/settings.js +82 -0
- package/samples/kanban/js/state.js +118 -0
- package/samples/kanban/js/ui.js +49 -0
- package/scripts/generate-client.mjs +344 -0
- package/src/generated/operations.js +9772 -0
- package/src/generated/schema.js +8982 -0
- package/src/index.js +85 -0
- package/src/runtime/client.js +1208 -0
- package/src/runtime/error.js +29 -0
- package/src/runtime/http.js +174 -0
- package/src/runtime/request-shape.js +35 -0
- package/src/runtime/schema.js +206 -0
- package/src/runtime/suggest.js +74 -0
- 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, '&').replace(/</g, '<')
|
|
49
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
+
}
|