crewswarm 0.9.2 → 0.9.4
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/README.md +22 -9
- package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js +1 -0
- package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js +1 -0
- package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
- package/apps/dashboard/dist/assets/index-BeVllEj_.js +2 -0
- package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
- package/apps/dashboard/dist/assets/{index-CF0aJRtC.css → index-D-sRshvg.css} +1 -1
- package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
- package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js +1 -0
- package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
- package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js +1 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js +1 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +1 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
- package/apps/dashboard/dist/index.html +135 -15
- package/apps/dashboard/dist/index.html.br +0 -0
- package/apps/dashboard/dist/index.html.gz +0 -0
- package/apps/vibe/README.md +2 -2
- package/apps/vibe/package.json +1 -1
- package/apps/vibe/server.mjs +101 -56
- package/crew-lead.mjs +34 -4
- package/lib/bridges/cli-executor.mjs +1 -1
- package/lib/bridges/gateway-ws.mjs +4 -0
- package/lib/browser/passthrough-stderr.js +1 -0
- package/lib/chat/project-messages.mjs +3 -5
- package/lib/cli-process-tracker.mjs +3 -2
- package/lib/contacts/identity-linker.mjs +1 -0
- package/lib/crew-judge/judge.mjs +19 -18
- package/lib/crew-lead/agent-manager.mjs +1 -1
- package/lib/crew-lead/background.mjs +14 -1
- package/lib/crew-lead/chat-handler.mjs +38 -1
- package/lib/crew-lead/http-server.mjs +106 -57
- package/lib/crew-lead/llm-caller.mjs +24 -8
- package/lib/crew-lead/prompts.mjs +14 -1
- package/lib/crew-lead/tools.mjs +3 -2
- package/lib/crew-lead/wave-dispatcher.mjs +19 -5
- package/lib/crew-lead/ws-router.mjs +219 -27
- package/lib/engines/crew-cli.mjs +1 -1
- package/lib/engines/engine-registry.mjs +14 -3
- package/lib/engines/rt-envelope.mjs +1 -0
- package/lib/engines/runners.mjs +28 -4
- package/lib/gemini-cli-passthrough-noise.mjs +1 -1
- package/lib/integrations/code-search.mjs +4 -3
- package/lib/memory/shared-adapter.mjs +23 -10
- package/lib/pipeline/manager.mjs +2 -1
- package/lib/runtime/config.mjs +1 -1
- package/lib/runtime/paths.mjs +12 -8
- package/lib/runtime/spending.mjs +2 -1
- package/package.json +42 -14
- package/scripts/capture-build-flow.mjs +118 -0
- package/scripts/coverage-report.mjs +209 -0
- package/scripts/coverage-summary.mjs +47 -0
- package/scripts/dashboard-validation.mjs +76 -0
- package/scripts/dashboard.mjs +1667 -551
- package/scripts/generate-openapi.mjs +683 -277
- package/scripts/live-bridge-matrix.mjs +79 -0
- package/scripts/live-cli-matrix.mjs +166 -0
- package/scripts/live-crewchat-check.mjs +42 -0
- package/scripts/live-engine-matrix.mjs +50 -0
- package/scripts/live-provider-failover-matrix.mjs +107 -0
- package/scripts/live-provider-matrix.mjs +228 -0
- package/scripts/restart-all-from-repo.sh +4 -4
- package/scripts/restart-service.sh +12 -9
- package/scripts/smoke-dispatch.mjs +4 -1
- package/scripts/test-blast-radius.mjs +204 -0
- package/scripts/test-report-summary.mjs +88 -0
- package/scripts/test-reporter.mjs +651 -0
- package/scripts/test-rerun.mjs +136 -0
- package/scripts/tmux-bridge +130 -0
- package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js +0 -1
- package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +0 -1
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
- package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js +0 -1
- package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js +0 -1
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
- package/apps/dashboard/index.html +0 -6529
- package/apps/dashboard/package.json +0 -15
- package/apps/dashboard/src/app.js +0 -2828
- package/apps/dashboard/src/app.js.br +0 -0
- package/apps/dashboard/src/app.js.gz +0 -0
- package/apps/dashboard/src/chat/chat-actions.js +0 -1847
- package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
- package/apps/dashboard/src/chat/unified-messages.js +0 -327
- package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
- package/apps/dashboard/src/cli-process.js +0 -208
- package/apps/dashboard/src/cli-process.js.br +0 -0
- package/apps/dashboard/src/cli-process.js.gz +0 -0
- package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
- package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
- package/apps/dashboard/src/core/api.js +0 -18
- package/apps/dashboard/src/core/api.js.br +0 -0
- package/apps/dashboard/src/core/dom.js +0 -228
- package/apps/dashboard/src/core/dom.js.br +0 -0
- package/apps/dashboard/src/core/state.js +0 -91
- package/apps/dashboard/src/core/state.js.br +0 -0
- package/apps/dashboard/src/core/task-manager.js +0 -134
- package/apps/dashboard/src/core/task-manager.js.br +0 -0
- package/apps/dashboard/src/orchestration-status.js +0 -127
- package/apps/dashboard/src/orchestration-status.js.br +0 -0
- package/apps/dashboard/src/setup-wizard.js +0 -562
- package/apps/dashboard/src/setup-wizard.js.br +0 -0
- package/apps/dashboard/src/styles.css +0 -2085
- package/apps/dashboard/src/styles.css.br +0 -0
- package/apps/dashboard/src/styles.css.gz +0 -0
- package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
- package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
- package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/comms-tab.js +0 -955
- package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
- package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/engines-tab.js +0 -175
- package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/memory-tab.js +0 -182
- package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/models-tab.js +0 -450
- package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
- package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js +0 -663
- package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
- package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
- package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/services-tab.js +0 -202
- package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/settings-tab.js +0 -861
- package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/skills-tab.js +0 -284
- package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/spending-tab.js +0 -173
- package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
- package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
- package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/usage-tab.js +0 -390
- package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/waves-tab.js +0 -238
- package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
- package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
- package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
- package/apps/vibe/.crew/cost.json +0 -17
- package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
- package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
- package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
- package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
- package/apps/vibe/.crew/sandbox.json +0 -7
- package/apps/vibe/.crew/session.json +0 -330
- package/apps/vibe/.crew/training-data.jsonl +0 -0
- package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
- package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
- package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
- package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
- package/apps/vibe/ARCHITECTURE.md +0 -3393
- package/apps/vibe/QUICK-REFERENCE.md +0 -211
- package/apps/vibe/ROADMAP.md +0 -41
- package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
- package/apps/vibe/VISUAL-GUIDE.md +0 -378
- package/apps/vibe/capture-demo.mjs +0 -160
- package/apps/vibe/capture-full-demo.mjs +0 -255
- package/apps/vibe/capture-quickstart.mjs +0 -256
- package/apps/vibe/capture-vibe-assets.mjs +0 -71
- package/apps/vibe/capture-vibe-video.mjs +0 -260
- package/apps/vibe/check-buttons.js +0 -41
- package/apps/vibe/diagnose.html +0 -106
- package/apps/vibe/fix-buttons.js +0 -103
- package/apps/vibe/index.html +0 -3404
- package/apps/vibe/package-lock.json +0 -920
- package/apps/vibe/scripts/studio-pty-host.py +0 -117
- package/apps/vibe/src/main.js +0 -2940
- package/apps/vibe/src/register-all-languages.js +0 -98
- package/apps/vibe/start-studio.sh +0 -11
- package/apps/vibe/test/accessibility-tests.js +0 -77
- package/apps/vibe/test/browser-performance-audit.mjs +0 -205
- package/apps/vibe/test/performance-tests.js +0 -120
- package/apps/vibe/test/security-tests.js +0 -213
- package/apps/vibe/tests/e2e.local.mjs +0 -54
- package/apps/vibe/tests/server.smoke.mjs +0 -106
- package/apps/vibe/update_website.mjs +0 -74
- package/apps/vibe/vite.config.js +0 -19
- package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
- package/lib/engines/rt-envelope.mjs.backup-current +0 -870
|
Binary file
|
|
@@ -1,654 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contacts Tab — Unified contact management across WhatsApp, Telegram, etc.
|
|
3
|
-
* Deps: getJSON, postJSON (core/api), escHtml, showNotification (core/dom), state (core/state)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getJSON, postJSON } from '../core/api.js';
|
|
7
|
-
import { escHtml, showNotification } from '../core/dom.js';
|
|
8
|
-
import { state } from '../core/state.js';
|
|
9
|
-
|
|
10
|
-
let _contactsData = {};
|
|
11
|
-
let _allContacts = [];
|
|
12
|
-
let _currentFilters = {
|
|
13
|
-
search: '',
|
|
14
|
-
platform: '',
|
|
15
|
-
sortBy: 'name',
|
|
16
|
-
letter: 'all'
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function showContacts() {
|
|
20
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
21
|
-
document.getElementById('contactsView').classList.add('active');
|
|
22
|
-
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
23
|
-
const nav = document.getElementById('navContacts');
|
|
24
|
-
if (nav) nav.classList.add('active');
|
|
25
|
-
loadContacts();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ── Load contacts ─────────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
export async function loadContacts() {
|
|
31
|
-
const list = document.getElementById('contactsList');
|
|
32
|
-
|
|
33
|
-
list.innerHTML = '<div class="meta" style="padding:20px;">Loading contacts...</div>';
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const data = await getJSON('/api/contacts');
|
|
37
|
-
const contacts = data.contacts || [];
|
|
38
|
-
|
|
39
|
-
_contactsData = {};
|
|
40
|
-
contacts.forEach(c => { _contactsData[c.contact_id] = c; });
|
|
41
|
-
_allContacts = contacts;
|
|
42
|
-
|
|
43
|
-
if (!contacts.length) {
|
|
44
|
-
list.innerHTML = '<div class="meta" style="padding:20px;">No contacts yet. Click "+ New Contact" to add manually, or contacts are created automatically when someone messages the bot.</div>';
|
|
45
|
-
document.getElementById('contactsCount').innerHTML = '';
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
applyFiltersAndRender();
|
|
50
|
-
|
|
51
|
-
} catch(e) {
|
|
52
|
-
list.innerHTML = '<div class="meta" style="padding:20px;color:var(--red-hi);">Failed to load contacts: ' + escHtml(e.message) + '</div>';
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ── Filtering and Sorting ─────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
export function applyContactFilters() {
|
|
59
|
-
const searchInput = document.getElementById('contactsSearch');
|
|
60
|
-
const platformFilter = document.getElementById('contactsPlatformFilter');
|
|
61
|
-
const sortBy = document.getElementById('contactsSortBy');
|
|
62
|
-
|
|
63
|
-
_currentFilters.search = searchInput.value.toLowerCase().trim();
|
|
64
|
-
_currentFilters.platform = platformFilter.value;
|
|
65
|
-
_currentFilters.sortBy = sortBy.value;
|
|
66
|
-
|
|
67
|
-
applyFiltersAndRender();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function filterByLetter(letter) {
|
|
71
|
-
_currentFilters.letter = letter;
|
|
72
|
-
|
|
73
|
-
// Update active button
|
|
74
|
-
document.querySelectorAll('.alpha-filter').forEach(btn => {
|
|
75
|
-
btn.classList.remove('active');
|
|
76
|
-
btn.style.background = 'var(--bg-1)';
|
|
77
|
-
btn.style.color = 'var(--text-2)';
|
|
78
|
-
});
|
|
79
|
-
const activeBtn = document.querySelector(`[data-letter="${letter}"]`);
|
|
80
|
-
if (activeBtn) {
|
|
81
|
-
activeBtn.classList.add('active');
|
|
82
|
-
activeBtn.style.background = 'var(--purple)';
|
|
83
|
-
activeBtn.style.color = '#fff';
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
applyFiltersAndRender();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function applyFiltersAndRender() {
|
|
90
|
-
let filtered = [..._allContacts];
|
|
91
|
-
|
|
92
|
-
// Filter by search term
|
|
93
|
-
if (_currentFilters.search) {
|
|
94
|
-
filtered = filtered.filter(c => {
|
|
95
|
-
const searchText = `${c.display_name} ${c.phone_number || ''} ${c.email || ''} ${c.notes || ''} ${JSON.stringify(c.preferences)} ${JSON.stringify(c.tags)}`.toLowerCase();
|
|
96
|
-
return searchText.includes(_currentFilters.search);
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Filter by platform
|
|
101
|
-
if (_currentFilters.platform) {
|
|
102
|
-
filtered = filtered.filter(c => {
|
|
103
|
-
if (c.platform === _currentFilters.platform) return true;
|
|
104
|
-
const links = c.platform_links || {};
|
|
105
|
-
return !!links[_currentFilters.platform];
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Filter by letter
|
|
110
|
-
if (_currentFilters.letter !== 'all') {
|
|
111
|
-
filtered = filtered.filter(c => {
|
|
112
|
-
const firstChar = (c.display_name || '').charAt(0).toUpperCase();
|
|
113
|
-
if (_currentFilters.letter === '#') {
|
|
114
|
-
return !/[A-Z]/.test(firstChar);
|
|
115
|
-
}
|
|
116
|
-
return firstChar === _currentFilters.letter;
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Sort
|
|
121
|
-
if (_currentFilters.sortBy === 'name') {
|
|
122
|
-
filtered.sort((a, b) => {
|
|
123
|
-
const nameA = (a.display_name || a.phone_number || '').toLowerCase();
|
|
124
|
-
const nameB = (b.display_name || b.phone_number || '').toLowerCase();
|
|
125
|
-
return nameA.localeCompare(nameB);
|
|
126
|
-
});
|
|
127
|
-
} else if (_currentFilters.sortBy === 'recent') {
|
|
128
|
-
filtered.sort((a, b) => b.last_seen - a.last_seen);
|
|
129
|
-
} else if (_currentFilters.sortBy === 'messages') {
|
|
130
|
-
filtered.sort((a, b) => b.message_count - a.message_count);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Update count
|
|
134
|
-
const countEl = document.getElementById('contactsCount');
|
|
135
|
-
if (countEl) {
|
|
136
|
-
const total = _allContacts.length;
|
|
137
|
-
const showing = filtered.length;
|
|
138
|
-
if (showing === total) {
|
|
139
|
-
countEl.innerHTML = `Showing all ${total} contacts`;
|
|
140
|
-
} else {
|
|
141
|
-
countEl.innerHTML = `Showing ${showing} of ${total} contacts`;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
renderContactsList(filtered);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ── Render ────────────────────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
function renderContactsList(contacts) {
|
|
151
|
-
const list = document.getElementById('contactsList');
|
|
152
|
-
|
|
153
|
-
if (!contacts.length) {
|
|
154
|
-
list.innerHTML = '<div class="meta" style="padding:20px;">No contacts match your filters.</div>';
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
list.innerHTML = contacts.map(c => {
|
|
159
|
-
const id = c.contact_id;
|
|
160
|
-
const safeId = escHtml(id);
|
|
161
|
-
const name = escHtml(c.display_name || c.phone_number || 'Unknown');
|
|
162
|
-
const platformLinks = c.platform_links || {};
|
|
163
|
-
const preferences = c.preferences || {};
|
|
164
|
-
const tags = c.tags || [];
|
|
165
|
-
|
|
166
|
-
// Platform badges
|
|
167
|
-
const platforms = [];
|
|
168
|
-
if (c.platform === 'whatsapp' || platformLinks.whatsapp) platforms.push('<span style="background:#25D366;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">WhatsApp</span>');
|
|
169
|
-
if (c.platform === 'telegram' || platformLinks.telegram) platforms.push('<span style="background:#0088cc;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Telegram</span>');
|
|
170
|
-
if (c.platform === 'twitter' || platformLinks.twitter) platforms.push('<span style="background:#1DA1F2;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Twitter</span>');
|
|
171
|
-
if (c.platform === 'instagram' || platformLinks.instagram) platforms.push('<span style="background:#E4405F;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Instagram</span>');
|
|
172
|
-
if (c.platform === 'tiktok' || platformLinks.tiktok) platforms.push('<span style="background:#000000;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">TikTok</span>');
|
|
173
|
-
if (c.platform === 'slack' || platformLinks.slack) platforms.push('<span style="background:#4A154B;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Slack</span>');
|
|
174
|
-
if (c.platform === 'web' || platformLinks.web) platforms.push('<span style="background:#666;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Web</span>');
|
|
175
|
-
if (c.platform === 'website' || platformLinks.website) platforms.push('<span style="background:#666;color:#fff;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600;">Website</span>');
|
|
176
|
-
|
|
177
|
-
// Last seen
|
|
178
|
-
const lastSeenText = formatRelativeTime(c.last_seen);
|
|
179
|
-
|
|
180
|
-
// Preferences preview (compact)
|
|
181
|
-
const prefPreview = [];
|
|
182
|
-
if (preferences?.diet) prefPreview.push(`🍴 ${escHtml(preferences.diet)}`);
|
|
183
|
-
if (preferences?.allergies?.length) prefPreview.push(`⚠️ ${escHtml(preferences.allergies.join(', '))}`);
|
|
184
|
-
if (preferences?.spiceLevel) prefPreview.push(`🌶️ ${escHtml(preferences.spiceLevel)}`);
|
|
185
|
-
|
|
186
|
-
// Tags
|
|
187
|
-
const tagHtml = tags.length ? tags.map(t => `<span style="background:var(--bg-1);color:var(--text-3);padding:2px 6px;border-radius:4px;font-size:10px;">${escHtml(t)}</span>`).join(' ') : '';
|
|
188
|
-
|
|
189
|
-
// Expandable details
|
|
190
|
-
const phone = c.phone_number ? `<div><strong>Phone:</strong> ${escHtml(c.phone_number)}</div>` : '';
|
|
191
|
-
const email = c.email ? `<div><strong>Email:</strong> ${escHtml(c.email)}</div>` : '';
|
|
192
|
-
const notes = c.notes ? `<div style="margin-top:8px;"><strong>Notes:</strong> ${escHtml(c.notes)}</div>` : '';
|
|
193
|
-
const allPrefs = preferences && Object.keys(preferences).length > 0
|
|
194
|
-
? `<div style="margin-top:8px;"><strong>Preferences:</strong> <pre style="font-size:11px;margin-top:4px;background:var(--bg-1);padding:8px;border-radius:4px;overflow:auto;">${escHtml(JSON.stringify(preferences, null, 2))}</pre></div>`
|
|
195
|
-
: '';
|
|
196
|
-
|
|
197
|
-
return `
|
|
198
|
-
<div class="card contact-card" id="contact-card-${safeId}" data-contact-id="${safeId}">
|
|
199
|
-
<div id="contact-view-${safeId}">
|
|
200
|
-
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
|
|
201
|
-
<div style="flex:1;">
|
|
202
|
-
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px;">
|
|
203
|
-
<strong style="font-size:15px;">${name}</strong>
|
|
204
|
-
${platforms.join(' ')}
|
|
205
|
-
</div>
|
|
206
|
-
<div class="meta" style="font-size:12px;">
|
|
207
|
-
${c.message_count} messages · Last seen: ${lastSeenText}
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
${prefPreview.length ? `<div style="margin-bottom:10px;font-size:12px;color:var(--text-2);">${prefPreview.join(' · ')}</div>` : ''}
|
|
213
|
-
${tagHtml ? `<div style="margin-bottom:10px;display:flex;gap:4px;flex-wrap:wrap;">${tagHtml}</div>` : ''}
|
|
214
|
-
|
|
215
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
|
216
|
-
<button data-action="send-message" data-id="${safeId}" class="btn-green" style="font-size:12px;">💬 Send Message</button>
|
|
217
|
-
<button data-action="toggle-details" data-id="${safeId}" class="btn-ghost" style="font-size:12px;">📋 Details</button>
|
|
218
|
-
<button data-action="edit" data-id="${safeId}" class="btn-ghost" style="font-size:12px;">✏️ Edit</button>
|
|
219
|
-
<button data-action="delete" data-id="${safeId}" style="background:transparent;color:var(--text-3);border:1px solid var(--border);border-radius:6px;padding:4px 10px;cursor:pointer;font-size:12px;">🗑 Delete</button>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
<!-- Expandable details -->
|
|
224
|
-
<div id="contact-details-${safeId}" style="display:none;margin-top:14px;padding-top:14px;border-top:1px solid var(--border);font-size:13px;color:var(--text-2);">
|
|
225
|
-
${phone}
|
|
226
|
-
${email}
|
|
227
|
-
${notes}
|
|
228
|
-
${allPrefs}
|
|
229
|
-
</div>
|
|
230
|
-
|
|
231
|
-
<!-- Edit form -->
|
|
232
|
-
<div id="contact-edit-${safeId}" style="display:none;padding:12px;border-top:1px solid var(--border);margin-top:12px;">
|
|
233
|
-
<div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Display Name</label><input id="contact-name-${safeId}" type="text" value="${escHtml(c.display_name || '')}" style="margin-top:4px;width:100%;" /></div>
|
|
234
|
-
<div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Phone</label><input id="contact-phone-${safeId}" type="text" value="${escHtml(c.phone_number || '')}" style="margin-top:4px;width:100%;" /></div>
|
|
235
|
-
<div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Email</label><input id="contact-email-${safeId}" type="text" value="${escHtml(c.email || '')}" style="margin-top:4px;width:100%;" /></div>
|
|
236
|
-
<div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">📍 Location</label><input id="contact-location-${safeId}" type="text" value="${escHtml(c.last_location || '')}" placeholder="Grand Bend, Ontario, Canada" style="margin-top:4px;width:100%;" /></div>
|
|
237
|
-
<div style="margin-bottom:12px;padding:12px;background:var(--bg-1);border-radius:6px;">
|
|
238
|
-
<label style="font-size:12px;color:var(--text-2);display:block;margin-bottom:8px;font-weight:600;">🔗 Platform IDs</label>
|
|
239
|
-
<div style="font-size:10px;color:var(--text-3);margin-bottom:12px;">Primary: ${escHtml(c.contact_id)}</div>
|
|
240
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
|
241
|
-
<div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Telegram</label><input id="platform-telegram-${safeId}" type="text" placeholder="12345678" value="${escHtml(platformLinks.telegram || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
|
|
242
|
-
<div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">WhatsApp</label><input id="platform-whatsapp-${safeId}" type="text" placeholder="1234@s.whatsapp.net" value="${escHtml(platformLinks.whatsapp || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
|
|
243
|
-
<div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Twitter</label><input id="platform-twitter-${safeId}" type="text" placeholder="@username" value="${escHtml(platformLinks.twitter || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
|
|
244
|
-
<div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Instagram</label><input id="platform-instagram-${safeId}" type="text" placeholder="@username" value="${escHtml(platformLinks.instagram || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
|
|
245
|
-
<div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">TikTok</label><input id="platform-tiktok-${safeId}" type="text" placeholder="@username" value="${escHtml(platformLinks.tiktok || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
|
|
246
|
-
<div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Slack</label><input id="platform-slack-${safeId}" type="text" placeholder="U01234ABC" value="${escHtml(platformLinks.slack || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
|
|
247
|
-
<div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Website</label><input id="platform-website-${safeId}" type="text" placeholder="https://example.com" value="${escHtml(platformLinks.website || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
|
|
248
|
-
<div><label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Other</label><input id="platform-other-${safeId}" type="text" placeholder="Custom ID" value="${escHtml(platformLinks.other || '')}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" /></div>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
251
|
-
|
|
252
|
-
<!-- Preferences Section -->
|
|
253
|
-
<div style="margin-bottom:12px;padding:12px;background:var(--bg-1);border-radius:6px;">
|
|
254
|
-
<label style="font-size:12px;color:var(--text-2);display:block;margin-bottom:8px;font-weight:600;">🍴 Preferences</label>
|
|
255
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
|
256
|
-
<div>
|
|
257
|
-
<label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Diet</label>
|
|
258
|
-
<select id="pref-diet-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
|
|
259
|
-
<option value="">None</option>
|
|
260
|
-
<option value="omnivore" ${preferences?.diet === 'omnivore' ? 'selected' : ''}>Omnivore</option>
|
|
261
|
-
<option value="vegetarian" ${preferences?.diet === 'vegetarian' ? 'selected' : ''}>Vegetarian</option>
|
|
262
|
-
<option value="vegan" ${preferences?.diet === 'vegan' ? 'selected' : ''}>Vegan</option>
|
|
263
|
-
<option value="pescatarian" ${preferences?.diet === 'pescatarian' ? 'selected' : ''}>Pescatarian</option>
|
|
264
|
-
</select>
|
|
265
|
-
</div>
|
|
266
|
-
<div>
|
|
267
|
-
<label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Spice Level</label>
|
|
268
|
-
<select id="pref-spice-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
|
|
269
|
-
<option value="">Any</option>
|
|
270
|
-
<option value="mild" ${preferences?.spiceLevel === 'mild' ? 'selected' : ''}>Mild</option>
|
|
271
|
-
<option value="medium" ${preferences?.spiceLevel === 'medium' ? 'selected' : ''}>Medium</option>
|
|
272
|
-
<option value="hot" ${preferences?.spiceLevel === 'hot' ? 'selected' : ''}>Hot</option>
|
|
273
|
-
</select>
|
|
274
|
-
</div>
|
|
275
|
-
<div>
|
|
276
|
-
<label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Budget</label>
|
|
277
|
-
<select id="pref-budget-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
|
|
278
|
-
<option value="">Any</option>
|
|
279
|
-
<option value="budget" ${preferences?.budget === 'budget' ? 'selected' : ''}>Budget ($)</option>
|
|
280
|
-
<option value="moderate" ${preferences?.budget === 'moderate' ? 'selected' : ''}>Moderate ($$)</option>
|
|
281
|
-
<option value="upscale" ${preferences?.budget === 'upscale' ? 'selected' : ''}>Upscale ($$$)</option>
|
|
282
|
-
</select>
|
|
283
|
-
</div>
|
|
284
|
-
<div>
|
|
285
|
-
<label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Atmosphere</label>
|
|
286
|
-
<select id="pref-atmosphere-${safeId}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;">
|
|
287
|
-
<option value="">Any</option>
|
|
288
|
-
<option value="quiet" ${preferences?.atmosphere === 'quiet' ? 'selected' : ''}>Quiet</option>
|
|
289
|
-
<option value="lively" ${preferences?.atmosphere === 'lively' ? 'selected' : ''}>Lively</option>
|
|
290
|
-
<option value="romantic" ${preferences?.atmosphere === 'romantic' ? 'selected' : ''}>Romantic</option>
|
|
291
|
-
<option value="family-friendly" ${preferences?.atmosphere === 'family-friendly' ? 'selected' : ''}>Family-friendly</option>
|
|
292
|
-
</select>
|
|
293
|
-
</div>
|
|
294
|
-
<div style="grid-column:1/-1;">
|
|
295
|
-
<label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Allergies (comma-separated)</label>
|
|
296
|
-
<input id="pref-allergies-${safeId}" type="text" placeholder="shellfish, peanuts, gluten, dairy" value="${escHtml((preferences?.allergies || []).join(', '))}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" />
|
|
297
|
-
</div>
|
|
298
|
-
<div style="grid-column:1/-1;">
|
|
299
|
-
<label style="font-size:10px;color:var(--text-3);display:block;margin-bottom:2px;">Favorite Cuisines (comma-separated)</label>
|
|
300
|
-
<input id="pref-cuisines-${safeId}" type="text" placeholder="Thai, Mexican, Italian, Japanese" value="${escHtml((preferences?.favCuisines || []).join(', '))}" style="padding:6px 8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-card);font-size:12px;width:100%;" />
|
|
301
|
-
</div>
|
|
302
|
-
</div>
|
|
303
|
-
</div>
|
|
304
|
-
|
|
305
|
-
<div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Notes</label><textarea id="contact-notes-${safeId}" rows="3" style="margin-top:4px;width:100%;">${escHtml(c.notes || '')}</textarea></div>
|
|
306
|
-
<div style="margin-bottom:8px;"><label style="font-size:11px;color:var(--text-3);">Tags (comma-separated)</label><input id="contact-tags-${safeId}" type="text" value="${escHtml(tags.join(', '))}" style="margin-top:4px;width:100%;" /></div>
|
|
307
|
-
<div style="display:flex;gap:8px;">
|
|
308
|
-
<button data-action="save-edit" data-id="${safeId}" class="btn-green" style="font-size:12px;">💾 Save</button>
|
|
309
|
-
<button data-action="cancel-edit" data-id="${safeId}" class="btn-ghost" style="font-size:12px;">Cancel</button>
|
|
310
|
-
</div>
|
|
311
|
-
</div>
|
|
312
|
-
</div>
|
|
313
|
-
`;
|
|
314
|
-
}).join('');
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// ── Actions ───────────────────────────────────────────────────────────────────
|
|
318
|
-
|
|
319
|
-
export function toggleContactDetails(contactId) {
|
|
320
|
-
const detailsEl = document.getElementById('contact-details-' + contactId);
|
|
321
|
-
if (!detailsEl) return;
|
|
322
|
-
const isVisible = detailsEl.style.display !== 'none';
|
|
323
|
-
detailsEl.style.display = isVisible ? 'none' : 'block';
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export function toggleContactEdit(contactId) {
|
|
327
|
-
const viewEl = document.getElementById('contact-view-' + contactId);
|
|
328
|
-
const editEl = document.getElementById('contact-edit-' + contactId);
|
|
329
|
-
const detailsEl = document.getElementById('contact-details-' + contactId);
|
|
330
|
-
if (!viewEl || !editEl) return;
|
|
331
|
-
const isEditing = editEl.style.display !== 'none';
|
|
332
|
-
viewEl.style.display = isEditing ? '' : 'none';
|
|
333
|
-
editEl.style.display = isEditing ? 'none' : 'block';
|
|
334
|
-
if (detailsEl) detailsEl.style.display = 'none';
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export async function saveContactEdit(contactId) {
|
|
338
|
-
const name = document.getElementById('contact-name-' + contactId)?.value?.trim();
|
|
339
|
-
const phone = document.getElementById('contact-phone-' + contactId)?.value?.trim();
|
|
340
|
-
const email = document.getElementById('contact-email-' + contactId)?.value?.trim();
|
|
341
|
-
const location = document.getElementById('contact-location-' + contactId)?.value?.trim();
|
|
342
|
-
const notes = document.getElementById('contact-notes-' + contactId)?.value?.trim();
|
|
343
|
-
const tagsStr = document.getElementById('contact-tags-' + contactId)?.value?.trim();
|
|
344
|
-
const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
345
|
-
|
|
346
|
-
// Collect platform IDs from individual fields
|
|
347
|
-
const platformLinks = {};
|
|
348
|
-
const telegram = document.getElementById('platform-telegram-' + contactId)?.value?.trim();
|
|
349
|
-
const whatsapp = document.getElementById('platform-whatsapp-' + contactId)?.value?.trim();
|
|
350
|
-
const twitter = document.getElementById('platform-twitter-' + contactId)?.value?.trim();
|
|
351
|
-
const instagram = document.getElementById('platform-instagram-' + contactId)?.value?.trim();
|
|
352
|
-
const tiktok = document.getElementById('platform-tiktok-' + contactId)?.value?.trim();
|
|
353
|
-
const slack = document.getElementById('platform-slack-' + contactId)?.value?.trim();
|
|
354
|
-
const website = document.getElementById('platform-website-' + contactId)?.value?.trim();
|
|
355
|
-
const other = document.getElementById('platform-other-' + contactId)?.value?.trim();
|
|
356
|
-
|
|
357
|
-
if (telegram) platformLinks.telegram = telegram;
|
|
358
|
-
if (whatsapp) platformLinks.whatsapp = whatsapp;
|
|
359
|
-
if (twitter) platformLinks.twitter = twitter;
|
|
360
|
-
if (instagram) platformLinks.instagram = instagram;
|
|
361
|
-
if (tiktok) platformLinks.tiktok = tiktok;
|
|
362
|
-
if (slack) platformLinks.slack = slack;
|
|
363
|
-
if (website) platformLinks.website = website;
|
|
364
|
-
if (other) platformLinks.other = other;
|
|
365
|
-
|
|
366
|
-
// Collect preferences from form fields
|
|
367
|
-
const preferences = {};
|
|
368
|
-
const diet = document.getElementById('pref-diet-' + contactId)?.value?.trim();
|
|
369
|
-
const spice = document.getElementById('pref-spice-' + contactId)?.value?.trim();
|
|
370
|
-
const budget = document.getElementById('pref-budget-' + contactId)?.value?.trim();
|
|
371
|
-
const atmosphere = document.getElementById('pref-atmosphere-' + contactId)?.value?.trim();
|
|
372
|
-
const allergiesStr = document.getElementById('pref-allergies-' + contactId)?.value?.trim();
|
|
373
|
-
const cuisinesStr = document.getElementById('pref-cuisines-' + contactId)?.value?.trim();
|
|
374
|
-
|
|
375
|
-
if (diet) preferences.diet = diet;
|
|
376
|
-
if (spice) preferences.spiceLevel = spice;
|
|
377
|
-
if (budget) preferences.budget = budget;
|
|
378
|
-
if (atmosphere) preferences.atmosphere = atmosphere;
|
|
379
|
-
if (allergiesStr) preferences.allergies = allergiesStr.split(',').map(a => a.trim()).filter(Boolean);
|
|
380
|
-
if (cuisinesStr) preferences.favCuisines = cuisinesStr.split(',').map(c => c.trim()).filter(Boolean);
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
await postJSON('/api/contacts/update', {
|
|
384
|
-
contactId,
|
|
385
|
-
display_name: name,
|
|
386
|
-
phone_number: phone,
|
|
387
|
-
email,
|
|
388
|
-
last_location: location,
|
|
389
|
-
notes,
|
|
390
|
-
tags,
|
|
391
|
-
platform_links: platformLinks,
|
|
392
|
-
preferences
|
|
393
|
-
});
|
|
394
|
-
showNotification('Contact saved');
|
|
395
|
-
toggleContactEdit(contactId);
|
|
396
|
-
loadContacts();
|
|
397
|
-
} catch(e) {
|
|
398
|
-
showNotification('Failed: ' + e.message, true);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
export async function deleteContact(contactId) {
|
|
403
|
-
const contact = _contactsData[contactId];
|
|
404
|
-
const name = contact ? contact.display_name : contactId;
|
|
405
|
-
if (!confirm(`Delete contact "${name}"?\n\nMessage history will also be deleted.`)) return;
|
|
406
|
-
|
|
407
|
-
try {
|
|
408
|
-
await postJSON('/api/contacts/delete', { contactId });
|
|
409
|
-
showNotification('Contact deleted');
|
|
410
|
-
delete _contactsData[contactId];
|
|
411
|
-
_allContacts = _allContacts.filter(c => c.contact_id !== contactId);
|
|
412
|
-
applyFiltersAndRender();
|
|
413
|
-
} catch(e) {
|
|
414
|
-
showNotification('Failed: ' + e.message, true);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ── New Contact Modal ─────────────────────────────────────────────────────────
|
|
419
|
-
|
|
420
|
-
export function newContact() {
|
|
421
|
-
const modal = document.getElementById('modalOverlay') || createModalOverlay();
|
|
422
|
-
|
|
423
|
-
modal.innerHTML = `
|
|
424
|
-
<div style="background:var(--bg-card);border-radius:12px;padding:24px;max-width:500px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.4);">
|
|
425
|
-
<h3 style="margin:0 0 16px 0;">➕ New Contact</h3>
|
|
426
|
-
|
|
427
|
-
<div style="margin-bottom:12px;">
|
|
428
|
-
<label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Platform *</label>
|
|
429
|
-
<select id="newContactPlatform" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;">
|
|
430
|
-
<option value="telegram">Telegram</option>
|
|
431
|
-
<option value="whatsapp">WhatsApp</option>
|
|
432
|
-
<option value="twitter">Twitter</option>
|
|
433
|
-
<option value="instagram">Instagram</option>
|
|
434
|
-
<option value="tiktok">TikTok</option>
|
|
435
|
-
<option value="slack">Slack</option>
|
|
436
|
-
<option value="website">Website</option>
|
|
437
|
-
<option value="web">Web</option>
|
|
438
|
-
</select>
|
|
439
|
-
</div>
|
|
440
|
-
|
|
441
|
-
<div style="margin-bottom:12px;">
|
|
442
|
-
<label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Platform ID *</label>
|
|
443
|
-
<input id="newContactId" type="text" placeholder="12345678 (Telegram), @username (Twitter/IG/TikTok), URL (Website)" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
|
|
444
|
-
<div style="font-size:10px;color:var(--text-3);margin-top:4px;">
|
|
445
|
-
• Telegram: chat ID (e.g., 12345678)<br>
|
|
446
|
-
• WhatsApp: phone@s.whatsapp.net (e.g., 15551234567@s.whatsapp.net)<br>
|
|
447
|
-
• Twitter/Instagram/TikTok: @username<br>
|
|
448
|
-
• Website: URL (e.g., https://example.com)<br>
|
|
449
|
-
• Slack: user ID (e.g., U01234ABC)
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
|
-
|
|
453
|
-
<div style="margin-bottom:12px;">
|
|
454
|
-
<label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Display Name *</label>
|
|
455
|
-
<input id="newContactName" type="text" placeholder="John Doe" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
|
|
456
|
-
</div>
|
|
457
|
-
|
|
458
|
-
<div style="margin-bottom:12px;">
|
|
459
|
-
<label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Phone</label>
|
|
460
|
-
<input id="newContactPhone" type="text" placeholder="+1 310 905 0857" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
|
|
461
|
-
</div>
|
|
462
|
-
|
|
463
|
-
<div style="margin-bottom:12px;">
|
|
464
|
-
<label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Email</label>
|
|
465
|
-
<input id="newContactEmail" type="text" placeholder="john@example.com" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;" />
|
|
466
|
-
</div>
|
|
467
|
-
|
|
468
|
-
<div style="margin-bottom:16px;">
|
|
469
|
-
<label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Notes</label>
|
|
470
|
-
<textarea id="newContactNotes" rows="3" placeholder="Important client, prefers morning calls..." style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;"></textarea>
|
|
471
|
-
</div>
|
|
472
|
-
|
|
473
|
-
<div style="display:flex;gap:8px;justify-content:flex-end;">
|
|
474
|
-
<button onclick="document.getElementById('modalOverlay').style.display='none'" class="btn-ghost">Cancel</button>
|
|
475
|
-
<button onclick="window.createContact()" class="btn-green">Create Contact</button>
|
|
476
|
-
</div>
|
|
477
|
-
</div>
|
|
478
|
-
`;
|
|
479
|
-
|
|
480
|
-
modal.style.display = 'flex';
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export async function createContact() {
|
|
484
|
-
const platform = document.getElementById('newContactPlatform').value;
|
|
485
|
-
const platformId = document.getElementById('newContactId').value.trim();
|
|
486
|
-
const displayName = document.getElementById('newContactName').value.trim();
|
|
487
|
-
const phone = document.getElementById('newContactPhone').value.trim();
|
|
488
|
-
const email = document.getElementById('newContactEmail').value.trim();
|
|
489
|
-
const notes = document.getElementById('newContactNotes').value.trim();
|
|
490
|
-
|
|
491
|
-
if (!platform || !platformId || !displayName) {
|
|
492
|
-
showNotification('Platform, Platform ID, and Display Name are required', true);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const contactId = `${platform}:${platformId}`;
|
|
497
|
-
|
|
498
|
-
try {
|
|
499
|
-
await postJSON('/api/contacts/create', {
|
|
500
|
-
contact_id: contactId,
|
|
501
|
-
platform,
|
|
502
|
-
display_name: displayName,
|
|
503
|
-
phone_number: phone || null,
|
|
504
|
-
email: email || null,
|
|
505
|
-
notes: notes || null
|
|
506
|
-
});
|
|
507
|
-
showNotification('✅ Contact created');
|
|
508
|
-
document.getElementById('modalOverlay').style.display = 'none';
|
|
509
|
-
loadContacts();
|
|
510
|
-
} catch(e) {
|
|
511
|
-
showNotification('Failed: ' + e.message, true);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// ── Send Message Modal ────────────────────────────────────────────────────────
|
|
516
|
-
|
|
517
|
-
export function showSendMessageModal(contactId) {
|
|
518
|
-
const contact = _contactsData[contactId];
|
|
519
|
-
if (!contact) {
|
|
520
|
-
showNotification('Contact not found', true);
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const modal = document.getElementById('modalOverlay') || createModalOverlay();
|
|
525
|
-
|
|
526
|
-
const platformLinks = contact.platform_links || {};
|
|
527
|
-
const hasWhatsApp = contact.platform === 'whatsapp' || platformLinks.whatsapp;
|
|
528
|
-
const hasTelegram = contact.platform === 'telegram' || platformLinks.telegram;
|
|
529
|
-
|
|
530
|
-
modal.innerHTML = `
|
|
531
|
-
<div style="background:var(--bg-card);border-radius:12px;padding:24px;max-width:500px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.4);">
|
|
532
|
-
<h3 style="margin:0 0 16px 0;">💬 Send Message</h3>
|
|
533
|
-
<div style="margin-bottom:12px;">
|
|
534
|
-
<div style="font-size:13px;color:var(--text-2);margin-bottom:8px;">To: <strong>${escHtml(contact.display_name || contact.contact_id)}</strong></div>
|
|
535
|
-
</div>
|
|
536
|
-
<div style="margin-bottom:12px;">
|
|
537
|
-
<label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Platform</label>
|
|
538
|
-
<select id="sendMessagePlatform" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;">
|
|
539
|
-
${hasWhatsApp ? '<option value="whatsapp">WhatsApp</option>' : ''}
|
|
540
|
-
${hasTelegram ? '<option value="telegram">Telegram</option>' : ''}
|
|
541
|
-
${hasWhatsApp && hasTelegram ? '<option value="both">Both</option>' : ''}
|
|
542
|
-
</select>
|
|
543
|
-
</div>
|
|
544
|
-
<div style="margin-bottom:16px;">
|
|
545
|
-
<label style="font-size:12px;color:var(--text-3);display:block;margin-bottom:4px;">Message</label>
|
|
546
|
-
<textarea id="sendMessageText" rows="4" placeholder="Your message here..." style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-1);font-size:13px;"></textarea>
|
|
547
|
-
</div>
|
|
548
|
-
<div style="display:flex;gap:8px;justify-content:flex-end;">
|
|
549
|
-
<button onclick="document.getElementById('modalOverlay').style.display='none'" class="btn-ghost">Cancel</button>
|
|
550
|
-
<button onclick="window.sendContactMessage('${escHtml(contactId)}')" class="btn-green">Send</button>
|
|
551
|
-
</div>
|
|
552
|
-
</div>
|
|
553
|
-
`;
|
|
554
|
-
|
|
555
|
-
modal.style.display = 'flex';
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
export async function sendContactMessage(contactId) {
|
|
559
|
-
const platform = document.getElementById('sendMessagePlatform').value;
|
|
560
|
-
const message = document.getElementById('sendMessageText').value.trim();
|
|
561
|
-
|
|
562
|
-
if (!message) {
|
|
563
|
-
showNotification('Message cannot be empty', true);
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
try {
|
|
568
|
-
await postJSON('/api/contacts/send', { contactId, platform, message });
|
|
569
|
-
showNotification('✅ Message sent');
|
|
570
|
-
document.getElementById('modalOverlay').style.display = 'none';
|
|
571
|
-
} catch(e) {
|
|
572
|
-
showNotification('Failed: ' + e.message, true);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
577
|
-
|
|
578
|
-
function createModalOverlay() {
|
|
579
|
-
let modal = document.getElementById('modalOverlay');
|
|
580
|
-
if (!modal) {
|
|
581
|
-
modal = document.createElement('div');
|
|
582
|
-
modal.id = 'modalOverlay';
|
|
583
|
-
modal.style.cssText = 'display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:9999;align-items:center;justify-content:center;';
|
|
584
|
-
modal.onclick = (e) => { if (e.target === modal) modal.style.display = 'none'; };
|
|
585
|
-
document.body.appendChild(modal);
|
|
586
|
-
}
|
|
587
|
-
return modal;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function formatRelativeTime(timestamp) {
|
|
591
|
-
const now = Date.now();
|
|
592
|
-
const diff = now - timestamp;
|
|
593
|
-
const minutes = Math.floor(diff / 60000);
|
|
594
|
-
const hours = Math.floor(diff / 3600000);
|
|
595
|
-
const days = Math.floor(diff / 86400000);
|
|
596
|
-
|
|
597
|
-
if (minutes < 1) return 'just now';
|
|
598
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
599
|
-
if (hours < 24) return `${hours}h ago`;
|
|
600
|
-
if (days < 7) return `${days}d ago`;
|
|
601
|
-
return new Date(timestamp).toLocaleDateString();
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// ── Event Handlers ────────────────────────────────────────────────────────────
|
|
605
|
-
|
|
606
|
-
export function initContactsList() {
|
|
607
|
-
document.addEventListener('click', (e) => {
|
|
608
|
-
const action = e.target.getAttribute('data-action');
|
|
609
|
-
const id = e.target.getAttribute('data-id');
|
|
610
|
-
const letter = e.target.getAttribute('data-letter');
|
|
611
|
-
|
|
612
|
-
if (action === 'toggle-details') toggleContactDetails(id);
|
|
613
|
-
else if (action === 'edit') toggleContactEdit(id);
|
|
614
|
-
else if (action === 'save-edit') saveContactEdit(id);
|
|
615
|
-
else if (action === 'cancel-edit') toggleContactEdit(id);
|
|
616
|
-
else if (action === 'delete') deleteContact(id);
|
|
617
|
-
else if (action === 'send-message') showSendMessageModal(id);
|
|
618
|
-
else if (action === 'newContact') newContact();
|
|
619
|
-
else if (action === 'applyContactFilters') applyContactFilters();
|
|
620
|
-
else if (action === 'filterByLetter') filterByLetter(letter);
|
|
621
|
-
else if (action === 'loadContacts') loadContacts();
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
// Real-time search
|
|
625
|
-
const searchInput = document.getElementById('contactsSearch');
|
|
626
|
-
if (searchInput) {
|
|
627
|
-
searchInput.addEventListener('input', () => {
|
|
628
|
-
_currentFilters.search = searchInput.value.toLowerCase().trim();
|
|
629
|
-
applyFiltersAndRender();
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Platform filter change
|
|
634
|
-
const platformFilter = document.getElementById('contactsPlatformFilter');
|
|
635
|
-
if (platformFilter) {
|
|
636
|
-
platformFilter.addEventListener('change', applyContactFilters);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Sort change
|
|
640
|
-
const sortBy = document.getElementById('contactsSortBy');
|
|
641
|
-
if (sortBy) {
|
|
642
|
-
sortBy.addEventListener('change', applyContactFilters);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// Export to window for HTML onclick handlers
|
|
647
|
-
if (typeof window !== 'undefined') {
|
|
648
|
-
window.loadContacts = loadContacts;
|
|
649
|
-
window.searchContacts = applyContactFilters;
|
|
650
|
-
window.newContact = newContact;
|
|
651
|
-
window.createContact = createContact;
|
|
652
|
-
window.sendContactMessage = sendContactMessage;
|
|
653
|
-
window.filterByLetter = filterByLetter;
|
|
654
|
-
}
|