diff --git a/disciple-tools-homescreen-apps.php b/disciple-tools-homescreen-apps.php
index a05b151..eb5762e 100755
--- a/disciple-tools-homescreen-apps.php
+++ b/disciple-tools-homescreen-apps.php
@@ -93,6 +93,7 @@ private function __construct() {
require_once( 'magic-link/link-zume.php' );
require_once( 'magic-link/link-waha.php' );
require_once( 'magic-link/my-coaching.php' );
+ require_once( 'magic-link/my-contacts/my-contacts.php' );
// require_once( 'magic-link/dispatcher-contacts.php' );
require_once( 'magic-link/dispatcher-magic-link.php' );
$this->i18n();
diff --git a/magic-link/my-contacts/my-contacts.css b/magic-link/my-contacts/my-contacts.css
new file mode 100644
index 0000000..8009c60
--- /dev/null
+++ b/magic-link/my-contacts/my-contacts.css
@@ -0,0 +1,729 @@
+:root {
+ --primary-color: #3f729b;
+ --success-color: #4caf50;
+ --warning-color: #ff9800;
+ --danger-color: #f44336;
+ --border-color: #e0e0e0;
+ --bg-color: #f5f5f5;
+ --card-bg: #ffffff;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+ margin: 0;
+ padding: 0;
+ background-color: var(--bg-color);
+}
+
+.my-contacts-container {
+ display: grid;
+ grid-template-columns: 320px 1fr;
+ gap: 16px;
+ height: 100vh;
+ padding: 16px;
+}
+
+.panel {
+ background: var(--card-bg);
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.12);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.panel-header {
+ padding: 16px;
+ border-bottom: 1px solid var(--border-color);
+ font-weight: 600;
+ font-size: 16px;
+ background: var(--card-bg);
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+}
+
+.panel-header span {
+ color: #666;
+ font-weight: normal;
+ font-size: 14px;
+}
+
+.panel-header .header-contact-name,
+.panel-header #mobile-contact-name {
+ color: var(--primary-color);
+ font-weight: 600;
+ font-size: 16px;
+}
+
+.panel-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px;
+}
+
+/* Search input */
+.search-input {
+ display: block;
+ width: 100%;
+ flex-basis: 100%;
+ margin-top: 4px;
+ padding: 8px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ font-size: 14px;
+}
+
+.search-input:focus {
+ outline: none;
+ border-color: var(--primary-color);
+}
+
+/* Contact List */
+.contact-item {
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ margin-bottom: 8px;
+ cursor: pointer;
+ background: var(--card-bg);
+ transition: all 0.2s ease;
+}
+
+.contact-item:hover {
+ border-color: var(--primary-color);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.contact-item.selected {
+ border-color: var(--primary-color);
+ background: #e3f2fd;
+}
+
+.contact-name {
+ font-weight: 600;
+ margin-bottom: 4px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.contact-meta {
+ font-size: 12px;
+ color: #666;
+}
+
+.status-badge {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 10px;
+ font-size: 11px;
+ font-weight: 500;
+}
+
+.source-badge {
+ display: inline-block;
+ padding: 2px 6px;
+ border-radius: 8px;
+ font-size: 10px;
+ font-weight: 500;
+ background: #e3f2fd;
+ color: #1565c0;
+}
+
+/* Contact Details */
+.contact-name-header {
+ font-size: 22px;
+ font-weight: 700;
+ color: var(--primary-color);
+ margin: 0 0 20px 0;
+ padding-bottom: 12px;
+ border-bottom: 2px solid var(--primary-color);
+}
+
+.details-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 24px;
+ height: 100%;
+}
+
+.details-column {
+ overflow-y: auto;
+}
+
+.details-column h3 {
+ margin: 0 0 16px 0;
+ font-size: 14px;
+ color: #333;
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: 8px;
+}
+
+.detail-tile {
+ margin-bottom: 20px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.detail-tile:last-child {
+ border-bottom: none;
+}
+
+.tile-header {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--primary-color);
+ margin-bottom: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.detail-section {
+ margin-bottom: 12px;
+}
+
+.detail-label {
+ font-size: 11px;
+ color: #888;
+ text-transform: uppercase;
+ margin-bottom: 2px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.field-icon {
+ color: #aaa;
+ opacity: 0.7;
+}
+
+img.field-icon {
+ width: 12px;
+ height: 12px;
+ object-fit: contain;
+ filter: grayscale(100%);
+}
+
+i.field-icon {
+ font-size: 12px;
+ width: 12px;
+ text-align: center;
+ color: black;
+}
+
+.detail-value {
+ font-size: 14px;
+ white-space: pre-line;
+}
+
+.detail-value.empty-value {
+ color: #999;
+}
+
+.detail-empty {
+ color: #999;
+ font-style: italic;
+}
+
+.dt-record-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 12px;
+ padding: 8px 14px;
+ background: var(--primary-color);
+ color: white;
+ text-decoration: none;
+ border-radius: 4px;
+ font-size: 13px;
+ font-weight: 500;
+ transition: background 0.2s ease;
+}
+
+.dt-record-link:hover {
+ background: #2d5a7b;
+}
+
+/* Edit mode */
+.edit-icon {
+ cursor: pointer;
+ margin-left: 6px;
+ color: #666;
+ font-size: 14px;
+ transition: color 0.2s ease;
+}
+
+.edit-icon:hover {
+ color: var(--primary-color);
+}
+
+.detail-section .edit-mode {
+ display: none;
+}
+
+.detail-section.editing .view-mode,
+.detail-section.editing .detail-label {
+ display: none;
+}
+
+.detail-section.editing .edit-mode {
+ display: block;
+}
+
+.detail-section.saving .edit-mode {
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+.field-saving-spinner {
+ display: inline-block;
+ width: 12px;
+ height: 12px;
+ border: 2px solid var(--border-color);
+ border-top-color: var(--primary-color);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-left: 8px;
+ vertical-align: middle;
+}
+
+/* DT Component Overrides for Magic Link */
+.edit-mode dt-text,
+.edit-mode dt-textarea,
+.edit-mode dt-number,
+.edit-mode dt-single-select,
+.edit-mode dt-multi-select,
+.edit-mode dt-date,
+.edit-mode dt-multi-text,
+.edit-mode dt-tags,
+.edit-mode dt-connection,
+.edit-mode dt-location {
+ display: block;
+ width: 100%;
+}
+
+/* Activity/Comments list */
+.activity-list {
+ margin-top: 0;
+}
+
+.activity-item {
+ padding: 12px;
+ background: var(--bg-color);
+ border-radius: 6px;
+ margin-bottom: 8px;
+}
+
+.activity-item.type-comment {
+ border-left: 3px solid var(--primary-color);
+}
+
+.activity-item.type-activity {
+ border-left: 3px solid #999;
+}
+
+.activity-author {
+ font-weight: 600;
+ font-size: 13px;
+}
+
+.activity-date {
+ font-size: 11px;
+ color: #666;
+ margin-left: 8px;
+}
+
+.activity-type-badge {
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 8px;
+ margin-left: 8px;
+ text-transform: uppercase;
+}
+
+.activity-type-badge.comment {
+ background: #e3f2fd;
+ color: #1565c0;
+}
+
+.activity-type-badge.activity {
+ background: #f5f5f5;
+ color: #666;
+}
+
+/* Collapsible activity group */
+.activity-group {
+ margin-bottom: 8px;
+}
+
+.activity-group-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background: var(--bg-color);
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 13px;
+ color: #666;
+ border-left: 3px solid #ccc;
+}
+
+.activity-group-header:hover {
+ background: #eee;
+}
+
+.activity-group-arrow {
+ transition: transform 0.2s ease;
+ font-size: 10px;
+}
+
+.activity-group.expanded .activity-group-arrow {
+ transform: rotate(90deg);
+}
+
+.activity-group-content {
+ display: none;
+ padding-left: 20px;
+ margin-top: 4px;
+}
+
+.activity-group.expanded .activity-group-content {
+ display: block;
+}
+
+.activity-compact-item {
+ padding: 4px 0;
+ font-size: 12px;
+ color: #666;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.activity-compact-item:last-child {
+ border-bottom: none;
+}
+
+.activity-compact-author {
+ font-weight: 500;
+ color: #333;
+}
+
+.activity-compact-date {
+ color: #999;
+ margin-left: 8px;
+}
+
+.activity-content {
+ margin-top: 6px;
+ font-size: 14px;
+ white-space: pre-wrap;
+}
+
+.activity-content a {
+ color: var(--primary-color);
+ word-break: break-all;
+}
+
+.mention-tag {
+ color: var(--primary-color);
+ font-weight: 500;
+ background: #e3f2fd;
+ padding: 1px 4px;
+ border-radius: 3px;
+}
+
+/* Comment input */
+.comment-input-section {
+ margin-bottom: 16px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.comment-input-wrapper {
+ position: relative;
+}
+
+.comment-textarea {
+ width: 100%;
+ min-height: 80px;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ font-size: 14px;
+ font-family: inherit;
+ resize: vertical;
+}
+
+.comment-textarea:focus {
+ outline: none;
+ border-color: var(--primary-color);
+}
+
+.comment-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.comment-submit-btn {
+ background: var(--primary-color);
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-size: 13px;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.comment-submit-btn:hover {
+ background: #2d5a7b;
+}
+
+.comment-submit-btn:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
+
+/* @mention dropdown */
+.mention-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background: white;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ max-height: 200px;
+ overflow-y: auto;
+ display: none;
+ z-index: 1000;
+ margin-top: 4px;
+}
+
+.mention-dropdown.show {
+ display: block;
+}
+
+.mention-item {
+ padding: 10px 12px;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.mention-item:hover,
+.mention-item.active {
+ background: #e3f2fd;
+}
+
+/* Empty states */
+.empty-state {
+ text-align: center;
+ padding: 40px 20px;
+ color: #666;
+}
+
+.empty-state-icon {
+ font-size: 48px;
+ margin-bottom: 16px;
+}
+
+/* Loading */
+.loading {
+ text-align: center;
+ padding: 40px;
+}
+
+.loading-spinner {
+ width: 40px;
+ height: 40px;
+ border: 4px solid var(--border-color);
+ border-top-color: var(--primary-color);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Success message */
+.success-toast {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ background: var(--success-color);
+ color: white;
+ padding: 16px 24px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+ display: none;
+ z-index: 1000;
+}
+
+.success-toast.show {
+ display: block;
+ animation: slideIn 0.3s ease;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateY(100px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Mobile/Tablet Layout */
+@media (max-width: 1024px) {
+ .details-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .my-contacts-container {
+ grid-template-columns: 1fr;
+ height: 100vh;
+ padding: 0;
+ gap: 0;
+ }
+
+ /* Hide contacts list on mobile when contact is selected */
+ #contacts-panel.mobile-hidden {
+ display: none;
+ }
+
+ /* Hide details panel by default on mobile */
+ #details-panel {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+ border-radius: 0;
+ }
+
+ #details-panel.mobile-visible {
+ display: flex;
+ }
+
+ .panel {
+ max-height: none;
+ height: 100vh;
+ border-radius: 0;
+ }
+
+ .panel-header {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ }
+
+ .mobile-back-btn {
+ background: none;
+ border: none;
+ font-size: 18px;
+ cursor: pointer;
+ padding: 4px 8px;
+ margin-right: 8px;
+ }
+
+ .details-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.mobile-only {
+ display: none;
+}
+
+/* Mobile navigation arrows */
+.mobile-nav-arrows {
+ display: none;
+ margin-left: auto;
+ gap: 4px;
+}
+
+.nav-arrow {
+ background: none;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ padding: 4px 8px;
+ cursor: pointer;
+ color: var(--primary-color);
+ font-size: 18px;
+ line-height: 1;
+}
+
+.nav-arrow:disabled {
+ color: #ccc;
+ border-color: #eee;
+ cursor: not-allowed;
+}
+
+.nav-arrow:not(:disabled):hover {
+ background: var(--bg-color);
+}
+
+/* Mobile tabs */
+.mobile-tabs {
+ display: none;
+ border-bottom: 1px solid var(--border-color);
+ background: var(--card-bg);
+}
+
+.mobile-tab {
+ flex: 1;
+ padding: 12px 16px;
+ border: none;
+ background: none;
+ font-size: 14px;
+ font-weight: 500;
+ color: #666;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all 0.2s ease;
+}
+
+.mobile-tab.active {
+ color: var(--primary-color);
+ border-bottom-color: var(--primary-color);
+}
+
+@media (max-width: 1024px) {
+ .mobile-only {
+ display: inline-block;
+ }
+
+ .desktop-only {
+ display: none;
+ }
+
+ .mobile-nav-arrows {
+ display: flex;
+ }
+
+ .mobile-tabs {
+ display: flex;
+ }
+
+ .details-column {
+ display: none;
+ }
+
+ .details-column.mobile-tab-active {
+ display: block;
+ }
+}
diff --git a/magic-link/my-contacts/my-contacts.js b/magic-link/my-contacts/my-contacts.js
new file mode 100644
index 0000000..b010cc8
--- /dev/null
+++ b/magic-link/my-contacts/my-contacts.js
@@ -0,0 +1,742 @@
+const { createApp, ref, computed, onMounted, nextTick, watch } = Vue;
+
+const MyContactsApp = createApp({
+ setup() {
+ // Reactive state
+ const contacts = ref([]);
+ const filteredContacts = ref([]);
+ const selectedContactId = ref(null);
+ const selectedContact = ref(null);
+ const searchTerm = ref('');
+ const loading = ref(true);
+ const detailsLoading = ref(false);
+ const contactsCount = computed(() => filteredContacts.value.length);
+
+ // Comment state
+ const commentText = ref('');
+ const commentSubmitting = ref(false);
+
+ // Mention state
+ const mentionUsers = ref([]);
+ const mentionActiveIndex = ref(0);
+ const mentionStartPos = ref(-1);
+ const showMentionDropdown = ref(false);
+ let mentionSearchTimeout = null;
+
+ // Mobile state
+ const isMobileDetailsVisible = ref(false);
+ const mobileActiveTab = ref('details');
+
+ // Load contacts on mount
+ onMounted(() => {
+ loadContacts();
+ setupGlobalEventListeners();
+ });
+
+ // Watch search term to filter contacts
+ watch(searchTerm, (term) => {
+ if (!term) {
+ filteredContacts.value = contacts.value;
+ } else {
+ const lowerTerm = term.toLowerCase();
+ filteredContacts.value = contacts.value.filter(c =>
+ c.name.toLowerCase().includes(lowerTerm) ||
+ (c.overall_status && c.overall_status.toLowerCase().includes(lowerTerm)) ||
+ (c.seeker_path && c.seeker_path.toLowerCase().includes(lowerTerm))
+ );
+ }
+ });
+
+ // API helper
+ async function apiRequest(endpoint, data = {}) {
+ const response = await fetch(
+ `${myContactsApp.root}${myContactsApp.parts.root}/v1/${myContactsApp.parts.type}/${endpoint}`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': myContactsApp.nonce
+ },
+ body: JSON.stringify({
+ parts: myContactsApp.parts,
+ ...data
+ })
+ }
+ );
+ return response.json();
+ }
+
+ // Load contacts
+ async function loadContacts() {
+ loading.value = true;
+ try {
+ const data = await apiRequest('contacts');
+ contacts.value = data.contacts || [];
+ filteredContacts.value = contacts.value;
+ } catch (error) {
+ console.error('Error loading contacts:', error);
+ contacts.value = [];
+ filteredContacts.value = [];
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ // Select contact
+ async function selectContact(contactId) {
+ selectedContactId.value = contactId;
+ detailsLoading.value = true;
+
+ if (isMobile()) {
+ isMobileDetailsVisible.value = true;
+ }
+
+ try {
+ const contact = await apiRequest('contact', { contact_id: contactId });
+
+ if (contact.code) {
+ selectedContact.value = { error: contact.message || 'Unknown error' };
+ return;
+ }
+
+ // Group activity items
+ contact.groupedActivity = groupActivityItems(contact.activity || []);
+ selectedContact.value = contact;
+
+ await nextTick();
+ initMentionListeners();
+ } catch (error) {
+ console.error('Error loading contact details:', error);
+ selectedContact.value = { error: 'Error loading details' };
+ } finally {
+ detailsLoading.value = false;
+ }
+ }
+
+ // Group activity items
+ function groupActivityItems(items) {
+ const grouped = [];
+ let currentGroup = null;
+
+ items.forEach(item => {
+ if (item.type === 'comment') {
+ if (currentGroup) {
+ grouped.push({ type: 'activity-group', items: currentGroup });
+ currentGroup = null;
+ }
+ grouped.push(item);
+ } else {
+ if (!currentGroup) currentGroup = [];
+ currentGroup.push(item);
+ }
+ });
+
+ if (currentGroup) {
+ grouped.push({ type: 'activity-group', items: currentGroup });
+ }
+
+ return grouped;
+ }
+
+ // Toggle activity group
+ function toggleActivityGroup(index) {
+ const group = document.getElementById(`activity-group-${index}`);
+ if (group) {
+ group.classList.toggle('expanded');
+ }
+ }
+
+ // Submit comment
+ async function submitComment() {
+ const comment = commentText.value.trim();
+ if (!comment || !selectedContactId.value) return;
+
+ commentSubmitting.value = true;
+
+ try {
+ const result = await apiRequest('comment', {
+ contact_id: selectedContactId.value,
+ comment: comment
+ });
+
+ if (result.success || result.comment_id) {
+ // Add new comment to grouped activity
+ const newComment = {
+ type: 'comment',
+ id: result.comment_id,
+ content: comment,
+ author: result.author || 'You',
+ timestamp: Math.floor(Date.now() / 1000)
+ };
+
+ if (selectedContact.value && selectedContact.value.groupedActivity) {
+ selectedContact.value.groupedActivity.unshift(newComment);
+ }
+
+ commentText.value = '';
+ showSuccessToast('Comment added successfully!');
+ } else {
+ alert('Failed to add comment: ' + (result.message || 'Unknown error'));
+ }
+ } catch (error) {
+ console.error('Error posting comment:', error);
+ alert('Error posting comment');
+ } finally {
+ commentSubmitting.value = false;
+ }
+ }
+
+ // Mention functionality
+ function initMentionListeners() {
+ const textarea = document.getElementById('comment-textarea');
+ if (!textarea || textarea.hasAttribute('data-mention-init')) return;
+ textarea.setAttribute('data-mention-init', 'true');
+
+ textarea.addEventListener('input', handleMentionInput);
+ textarea.addEventListener('keydown', handleMentionKeydown);
+ }
+
+ function handleMentionInput(e) {
+ const text = e.target.value;
+ const cursorPos = e.target.selectionStart;
+ const textBeforeCursor = text.substring(0, cursorPos);
+ const lastAtIndex = textBeforeCursor.lastIndexOf('@');
+
+ if (lastAtIndex !== -1) {
+ const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
+
+ if (!textAfterAt.includes(' ') && !textAfterAt.includes('\n')) {
+ mentionStartPos.value = lastAtIndex;
+
+ clearTimeout(mentionSearchTimeout);
+ mentionSearchTimeout = setTimeout(() => {
+ searchMentionUsers(textAfterAt);
+ }, 200);
+ return;
+ }
+ }
+
+ hideMentionDropdown();
+ }
+
+ function handleMentionKeydown(e) {
+ if (!showMentionDropdown.value) return;
+
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ mentionActiveIndex.value = Math.min(mentionActiveIndex.value + 1, mentionUsers.value.length - 1);
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ mentionActiveIndex.value = Math.max(mentionActiveIndex.value - 1, 0);
+ } else if (e.key === 'Enter' && mentionUsers.value.length > 0) {
+ e.preventDefault();
+ selectMention(mentionUsers.value[mentionActiveIndex.value]);
+ } else if (e.key === 'Escape') {
+ hideMentionDropdown();
+ }
+ }
+
+ async function searchMentionUsers(search) {
+ if (search.length < 1) {
+ hideMentionDropdown();
+ return;
+ }
+
+ try {
+ const data = await apiRequest('users-mention', { search });
+ mentionUsers.value = data.users || [];
+ mentionActiveIndex.value = 0;
+
+ if (mentionUsers.value.length > 0) {
+ showMentionDropdown.value = true;
+ } else {
+ hideMentionDropdown();
+ }
+ } catch (error) {
+ console.error('Error searching users:', error);
+ hideMentionDropdown();
+ }
+ }
+
+ function selectMention(user) {
+ const textarea = document.getElementById('comment-textarea');
+ const text = textarea.value;
+ const cursorPos = textarea.selectionStart;
+
+ const beforeMention = text.substring(0, mentionStartPos.value);
+ const afterCursor = text.substring(cursorPos);
+
+ const mentionTextStr = `@[${user.name}](${user.ID}) `;
+ commentText.value = beforeMention + mentionTextStr + afterCursor;
+
+ nextTick(() => {
+ const newCursorPos = beforeMention.length + mentionTextStr.length;
+ textarea.setSelectionRange(newCursorPos, newCursorPos);
+ textarea.focus();
+ });
+
+ hideMentionDropdown();
+ }
+
+ function hideMentionDropdown() {
+ showMentionDropdown.value = false;
+ mentionUsers.value = [];
+ mentionStartPos.value = -1;
+ }
+
+ // Field editing
+ function toggleEditMode(fieldKey) {
+ const section = document.querySelector(`.detail-section[data-field-key="${fieldKey}"]`);
+ if (!section) return;
+
+ const isEditing = section.classList.contains('editing');
+
+ document.querySelectorAll('.detail-section.editing').forEach(el => {
+ if (el !== section) {
+ el.classList.remove('editing');
+ }
+ });
+
+ if (isEditing) {
+ section.classList.remove('editing');
+ } else {
+ section.classList.add('editing');
+ initFieldChangeListener(section);
+ }
+ }
+
+ function initFieldChangeListener(section) {
+ const editMode = section.querySelector('.edit-mode');
+ const component = editMode.querySelector('dt-text, dt-textarea, dt-number, dt-toggle, dt-date, dt-single-select, dt-multi-select, dt-multi-text, dt-tags, dt-connection, dt-location, dt-location-map');
+
+ if (!component || component.hasAttribute('data-listener-added')) return;
+
+ component.setAttribute('data-listener-added', 'true');
+
+ const fieldType = section.dataset.fieldType;
+
+ component.addEventListener('change', async (e) => {
+ const fieldKey = section.dataset.fieldKey;
+ const contactId = section.dataset.contactId;
+ const rawValue = e.detail?.newValue ?? e.detail?.value ?? component.value;
+
+ let newValue = rawValue;
+ if (window.DtWebComponents?.ComponentService?.convertValue) {
+ newValue = window.DtWebComponents.ComponentService.convertValue(
+ component.tagName,
+ rawValue
+ );
+ }
+
+ await saveFieldValue(contactId, fieldKey, fieldType, newValue, section);
+ });
+ }
+
+ const multiValueFieldTypes = ['multi_select', 'connection', 'tags', 'location', 'location_meta', 'communication_channel'];
+
+ async function saveFieldValue(contactId, fieldKey, fieldType, value, section) {
+ section.classList.add('saving');
+
+ try {
+ const result = await apiRequest('update-field', {
+ contact_id: parseInt(contactId),
+ field_key: fieldKey,
+ field_value: value
+ });
+
+ if (result.success) {
+ const viewMode = section.querySelector('.view-mode');
+ const displayValue = result.value || '-';
+ viewMode.textContent = displayValue;
+ viewMode.classList.toggle('empty-value', !result.value);
+
+ if (!multiValueFieldTypes.includes(fieldType)) {
+ section.classList.remove('editing');
+ }
+ showSuccessToast('Field updated');
+ } else {
+ alert(result.message || 'Failed to update field');
+ }
+ } catch (error) {
+ console.error('Error saving field:', error);
+ alert('Error saving field');
+ } finally {
+ section.classList.remove('saving');
+ }
+ }
+
+ // Global event listeners
+ function setupGlobalEventListeners() {
+ // Handle dt:get-data events for typeahead
+ document.addEventListener('dt:get-data', async function(e) {
+ if (!e.detail) return;
+
+ const { field, query, onSuccess, onError, postType } = e.detail;
+
+ try {
+ const data = await apiRequest('field-options', {
+ field: field,
+ query: query || '',
+ post_type: postType || 'contacts'
+ });
+
+ if (data.success && data.options) {
+ if (onSuccess && typeof onSuccess === 'function') {
+ onSuccess(data.options);
+ }
+ } else {
+ if (onError && typeof onError === 'function') {
+ onError(new Error(data.message || 'Failed to fetch options'));
+ }
+ }
+ } catch (err) {
+ console.error('Error fetching field options:', err);
+ if (onError && typeof onError === 'function') {
+ onError(err);
+ }
+ }
+ });
+
+ // Close edit mode when clicking outside
+ document.addEventListener('click', function(e) {
+ const editingSection = document.querySelector('.detail-section.editing');
+ if (!editingSection) return;
+
+ if (editingSection.contains(e.target)) return;
+ if (e.target.closest('.option-list, ul[class*="option"], li[tabindex]')) return;
+
+ editingSection.classList.remove('editing');
+ });
+ }
+
+ // Mobile helpers
+ function isMobile() {
+ return window.innerWidth <= 1024;
+ }
+
+ function hideMobileDetails() {
+ isMobileDetailsVisible.value = false;
+ }
+
+ function navigateContact(direction) {
+ const currentIndex = filteredContacts.value.findIndex(c => c.ID === selectedContactId.value);
+ if (currentIndex === -1) return;
+
+ const newIndex = currentIndex + direction;
+ if (newIndex >= 0 && newIndex < filteredContacts.value.length) {
+ selectContact(filteredContacts.value[newIndex].ID);
+ }
+ }
+
+ function canNavigate(direction) {
+ const currentIndex = filteredContacts.value.findIndex(c => c.ID === selectedContactId.value);
+ if (currentIndex === -1) return false;
+
+ const newIndex = currentIndex + direction;
+ return newIndex >= 0 && newIndex < filteredContacts.value.length;
+ }
+
+ // Utility functions
+ function escapeHtml(text) {
+ if (!text) return '';
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ function escapeAttr(text) {
+ if (text === null || text === undefined) return '';
+ return String(text).replace(/"/g, '"').replace(/'/g, ''').replace(//g, '>');
+ }
+
+ function formatTimestamp(timestamp) {
+ if (!timestamp) return '';
+ const date = new Date(timestamp * 1000);
+ return date.toLocaleString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit'
+ });
+ }
+
+ function formatActivityContent(text) {
+ if (!text) return '';
+
+ let formatted = escapeHtml(text);
+
+ formatted = formatted.replace(
+ /@\[([^\]]+)\]\((\d+)\)/g,
+ '@$1'
+ );
+
+ const urlRegex = /(https?:\/\/[^\s<]+)/g;
+ formatted = formatted.replace(
+ urlRegex,
+ '$1'
+ );
+
+ return formatted;
+ }
+
+ function showSuccessToast(message = 'Success!') {
+ const toast = document.getElementById('success-toast');
+ if (toast) {
+ toast.textContent = message;
+ toast.classList.add('show');
+ setTimeout(() => {
+ toast.classList.remove('show');
+ }, 3000);
+ }
+ }
+
+ // Field rendering helpers
+ function renderFieldIcon(field) {
+ if (field.icon && !field.icon.includes('undefined')) {
+ return ``;
+ }
+ if (field.font_icon && !field.font_icon.includes('undefined')) {
+ return ``;
+ }
+ return '';
+ }
+
+ // Expose methods to template
+ return {
+ // State
+ contacts,
+ filteredContacts,
+ selectedContactId,
+ selectedContact,
+ searchTerm,
+ loading,
+ detailsLoading,
+ contactsCount,
+ commentText,
+ commentSubmitting,
+ mentionUsers,
+ mentionActiveIndex,
+ showMentionDropdown,
+ isMobileDetailsVisible,
+ mobileActiveTab,
+
+ // Methods
+ loadContacts,
+ selectContact,
+ toggleActivityGroup,
+ submitComment,
+ selectMention,
+ hideMentionDropdown,
+ toggleEditMode,
+ hideMobileDetails,
+ navigateContact,
+ canNavigate,
+
+ // Helpers
+ escapeHtml,
+ escapeAttr,
+ formatTimestamp,
+ formatActivityContent,
+ renderFieldIcon
+ };
+ },
+
+ template: `
+
Loading contacts...
+No contacts found
+Error: {{ selectedContact.error }}
+Select a contact to view details
+No contact information available
+ + +No activity yet
+Loading contacts...
+Select a contact to view details
+