|
|
|
|
|
|
|
|
| (global => {
|
| 'use strict';
|
|
|
| let previousPostsData = [];
|
| let selectedPrevPostIds = new Set();
|
| let isRotationActive = false;
|
| let editPrevPostMedia = [];
|
| let addPrevPostMedia = [];
|
|
|
| let currentMetricsPage = 1;
|
| const METRICS_ITEMS_PER_PAGE = 5;
|
|
|
| const STATE_INTEGRITY_SALT = "FritreeRotationMatrixDynamicIntegrityVerificationSalt_2026";
|
|
|
| function sanitizeRotationInput(str) {
|
| if (!str) return '';
|
| return String(str)
|
| .replace(/&/g, '&')
|
| .replace(/</g, '<')
|
| .replace(/>/g, '>')
|
| .replace(/"/g, '"')
|
| .replace(/'/g, ''');
|
| }
|
|
|
| |
| |
|
|
| function isValidPostSchema(p) {
|
| return p && typeof p === 'object' && typeof p.id === 'string';
|
| }
|
|
|
| |
| |
|
|
| function getIconForTemplateText(text, categories) {
|
| const raw = (text || '').toLowerCase();
|
| const catStr = (categories || []).join(' ').toLowerCase();
|
|
|
| if (raw.includes('apartment') || raw.includes('house') || raw.includes('villa') || raw.includes('rent') || catStr.includes('estate') || catStr.includes('realestate')) {
|
| return '<i class="fa-solid fa-house" style="color: #0d9488; margin-right: 5px;"></i>';
|
| }
|
| if (raw.includes('sale') || raw.includes('discount') || raw.includes('promo') || raw.includes('deal') || catStr.includes('sale') || catStr.includes('ecom')) {
|
| return '<i class="fa-solid fa-tags" style="color: #f59e0b; margin-right: 5px;"></i>';
|
| }
|
| if (raw.includes('software') || raw.includes('saas') || raw.includes('tech') || raw.includes('code') || catStr.includes('tech') || catStr.includes('software')) {
|
| return '<i class="fa-solid fa-laptop-code" style="color: #6366f1; margin-right: 5px;"></i>';
|
| }
|
| if (raw.includes('support') || raw.includes('help') || raw.includes('service') || catStr.includes('service') || catStr.includes('support')) {
|
| return '<i class="fa-solid fa-headset" style="color: #ef4444; margin-right: 5px;"></i>';
|
| }
|
| if (raw.includes('crypto') || raw.includes('token') || raw.includes('coin') || raw.includes('wallet') || catStr.includes('crypto')) {
|
| return '<i class="fa-solid fa-wallet" style="color: #8b5cf6; margin-right: 5px;"></i>';
|
| }
|
| return '<i class="fa-solid fa-file-invoice" style="color: #64748b; margin-right: 5px;"></i>';
|
| }
|
|
|
|
|
|
|
|
|
| async function calculateStateChecksum() {
|
| const structuralConcat = previousPostsData.map(p => `${p.id}:${p.status}`).sort().join('||');
|
| if (typeof FritreeCrypto !== 'undefined' && typeof FritreeCrypto.signReceipt === 'function') {
|
| return await FritreeCrypto.signReceipt("ROT_MATRIX", structuralConcat.length, "verify", structuralConcat, STATE_INTEGRITY_SALT);
|
| }
|
| return "lite_hash_" + structuralConcat.length;
|
| }
|
|
|
| async function saveDynamicStateChecksum() {
|
| const computedSignature = await calculateStateChecksum();
|
| await FritreeStorage.set('local_rotation_matrix_integrity_sig', computedSignature);
|
| }
|
|
|
| async function runSymmetricDataIntegrityCheck() {
|
| if (previousPostsData.length === 0) return true;
|
| const savedSignature = await FritreeStorage.get('local_rotation_matrix_integrity_sig', '');
|
| if (!savedSignature) return true;
|
| const actualSignature = await calculateStateChecksum();
|
| return savedSignature === actualSignature;
|
| }
|
|
|
| async function savePrevPostsToLocal() {
|
| await FritreeStorage.set('local_previous_posts', previousPostsData);
|
| await saveDynamicStateChecksum();
|
| }
|
|
|
|
|
|
|
|
|
| async function initRotationModule() {
|
| try {
|
| const rawPrevPosts = await FritreeStorage.get('local_previous_posts', []);
|
| previousPostsData = Array.isArray(rawPrevPosts) ? rawPrevPosts.filter(isValidPostSchema) : [];
|
|
|
| const cachedSelected = await FritreeStorage.get('local_selected_prev_posts', []);
|
| selectedPrevPostIds = new Set(Array.isArray(cachedSelected) ? cachedSelected.filter(id => typeof id === 'string') : []);
|
|
|
| const activeState = await FritreeStorage.get('local_is_rotation_active', 'false');
|
| isRotationActive = (activeState === 'true');
|
|
|
|
|
| const isIntegrityValid = await runSymmetricDataIntegrityCheck();
|
| if (!isIntegrityValid) {
|
| console.log("[Fritree Crypto] Auto-healing Content Rotation Library signature to preserve modifications.");
|
| await savePrevPostsToLocal();
|
| }
|
|
|
| recalculateAllPostScores();
|
|
|
| const openModalBtn = document.getElementById('btn-open-previous-posts-modal');
|
| const disableRotationBtn = document.getElementById('btn-disable-prev-posts-mode');
|
|
|
| const searchInput = document.getElementById('prev-posts-search');
|
| const filterSelect = document.getElementById('prev-posts-filter-status');
|
| const sortSelect = document.getElementById('prev-posts-sort-by');
|
| const selectAllCheck = document.getElementById('prev-posts-select-all');
|
|
|
| const filterKeywordInput = document.getElementById('prev-posts-filter-keyword');
|
| const filterCategoryInput = document.getElementById('prev-posts-filter-category');
|
| const filterNotesInput = document.getElementById('prev-posts-filter-notes');
|
| const filterFollowUpDateInput = document.getElementById('prev-posts-filter-followup-date');
|
| const filterMinScoreInput = document.getElementById('prev-posts-filter-min-score');
|
| const filterMaxCharsInput = document.getElementById('prev-posts-filter-max-chars');
|
|
|
|
|
| if (openModalBtn) openModalBtn.addEventListener('click', showPreviousPostsModal);
|
| if (disableRotationBtn) disableRotationBtn.addEventListener('click', disableRotationModeGlobally);
|
|
|
| if (searchInput) searchInput.addEventListener('input', renderPreviousPostsGrid);
|
| if (filterSelect) filterSelect.addEventListener('change', renderPreviousPostsGrid);
|
| if (sortSelect) sortSelect.addEventListener('change', renderPreviousPostsGrid);
|
|
|
| if (filterKeywordInput) filterKeywordInput.addEventListener('input', renderPreviousPostsGrid);
|
| if (filterCategoryInput) filterCategoryInput.addEventListener('input', renderPreviousPostsGrid);
|
| if (filterNotesInput) filterNotesInput.addEventListener('input', renderPreviousPostsGrid);
|
| if (filterFollowUpDateInput) filterFollowUpDateInput.addEventListener('change', renderPreviousPostsGrid);
|
| if (filterMinScoreInput) filterMinScoreInput.addEventListener('input', renderPreviousPostsGrid);
|
| if (filterMaxCharsInput) filterMaxCharsInput.addEventListener('input', renderPreviousPostsGrid);
|
|
|
| if (selectAllCheck) {
|
| selectAllCheck.addEventListener('change', async (e) => {
|
| const isChecked = e.target.checked;
|
| document.querySelectorAll('.prev-post-item-checkbox').forEach(chk => {
|
| chk.checked = isChecked;
|
| const id = chk.value;
|
| if (isChecked) selectedPrevPostIds.add(id);
|
| else selectedPrevPostIds.delete(id);
|
| });
|
| await FritreeStorage.set('local_selected_prev_posts', Array.from(selectedPrevPostIds));
|
| updateRotationIndicatorUI();
|
| });
|
| }
|
|
|
| const addCurrentManualBtn = document.getElementById('btn-add-current-to-prev-library');
|
| const bulkFreezeBtn = document.getElementById('btn-prev-bulk-freeze');
|
| const bulkUnfreezeBtn = document.getElementById('btn-prev-bulk-unfreeze');
|
| const bulkDraftBtn = document.getElementById('btn-prev-bulk-draft');
|
| const bulkActiveBtn = document.getElementById('btn-prev-bulk-active');
|
| const bulkArchiveBtn = document.getElementById('btn-prev-bulk-archive');
|
| const bulkDeleteBtn = document.getElementById('btn-prev-bulk-delete');
|
|
|
| if (addCurrentManualBtn) addCurrentManualBtn.addEventListener('click', handleSaveCurrentToLibraryManually);
|
| if (bulkFreezeBtn) bulkFreezeBtn.addEventListener('click', () => bulkPrevPostsAction('freeze'));
|
| if (bulkUnfreezeBtn) bulkUnfreezeBtn.addEventListener('click', () => bulkPrevPostsAction('unfreeze'));
|
| if (bulkDraftBtn) bulkDraftBtn.addEventListener('click', () => bulkPrevPostsAction('draft'));
|
| if (bulkActiveBtn) bulkActiveBtn.addEventListener('click', () => bulkPrevPostsAction('active'));
|
| if (bulkArchiveBtn) bulkArchiveBtn.addEventListener('click', () => bulkPrevPostsAction('inactive'));
|
| if (bulkDeleteBtn) bulkDeleteBtn.addEventListener('click', () => bulkPrevPostsAction('delete'));
|
|
|
| const validateAddSpintaxBtn = document.getElementById('btn-validate-add-spintax');
|
| if (validateAddSpintaxBtn) validateAddSpintaxBtn.addEventListener('click', spintaxValidatorAddPost);
|
|
|
| const editDropzone = document.getElementById('edit-prev-post-dropzone');
|
| const editFileInput = document.getElementById('edit-prev-post-file-input');
|
| const closeEditModalBtn = document.getElementById('edit-prev-post-close-btn');
|
| const saveEditedBtn = document.getElementById('btn-save-edited-prev-post');
|
|
|
| if (editDropzone && editFileInput) {
|
| editDropzone.addEventListener('click', () => editFileInput.click());
|
| editFileInput.addEventListener('change', (e) => handleEditPrevPostFiles(e.target.files));
|
|
|
| editDropzone.addEventListener('dragover', (e) => { e.preventDefault(); editDropzone.style.borderColor = 'var(--primary)'; });
|
| editDropzone.addEventListener('dragleave', () => { editDropzone.style.borderColor = 'var(--border)'; });
|
| editDropzone.addEventListener('drop', (e) => {
|
| e.preventDefault();
|
| editDropzone.style.borderColor = 'var(--border)';
|
| if (e.dataTransfer.files.length > 0) handleEditPrevPostFiles(e.dataTransfer.files);
|
| });
|
| }
|
|
|
| if (closeEditModalBtn) {
|
| closeEditModalBtn.addEventListener('click', () => {
|
| const modal = document.getElementById('edit-prev-post-modal');
|
| if (modal) modal.style.display = 'none';
|
| });
|
| }
|
|
|
| if (saveEditedBtn) saveEditedBtn.addEventListener('click', saveEditedPreviousPost);
|
|
|
| const addDropzone = document.getElementById('add-prev-post-dropzone');
|
| const addFileInput = document.getElementById('add-prev-post-file-input');
|
| const submitNewPrevPostBtn = document.getElementById('btn-submit-new-prev-post');
|
|
|
| if (addDropzone && addFileInput) {
|
| addDropzone.addEventListener('click', () => addFileInput.click());
|
| addFileInput.addEventListener('change', (e) => handleAddPrevPostFiles(e.target.files));
|
|
|
| addDropzone.addEventListener('dragover', (e) => { e.preventDefault(); addDropzone.style.borderColor = 'var(--primary)'; });
|
| addDropzone.addEventListener('dragleave', () => { addDropzone.style.borderColor = 'var(--border)'; });
|
| addDropzone.addEventListener('drop', (e) => {
|
| e.preventDefault();
|
| addDropzone.style.borderColor = 'var(--border)';
|
| if (e.dataTransfer.files.length > 0) handleAddPrevPostFiles(e.dataTransfer.files);
|
| });
|
| }
|
|
|
| if (submitNewPrevPostBtn) submitNewPrevPostBtn.addEventListener('click', handleAddNewRotationPost);
|
|
|
| const enableRotationChk = document.getElementById('chk-enable-rotation');
|
| const modeSelect = document.getElementById('sel-rotation-mode');
|
|
|
| if (enableRotationChk) {
|
| enableRotationChk.addEventListener('change', async (e) => {
|
| const details = document.getElementById('rotation-config-details');
|
| if (details) details.style.display = e.target.checked ? 'grid' : 'none';
|
| updateRotationBadgeInModal();
|
|
|
| await saveAndApplyRotationSettings();
|
| });
|
| }
|
|
|
| if (modeSelect) {
|
| modeSelect.addEventListener('change', async () => {
|
|
|
| await saveAndApplyRotationSettings();
|
| });
|
| }
|
|
|
| bindModalTabs();
|
| startRotationActiveSchedulers();
|
|
|
| updateRotationIndicatorUI();
|
| updateRotationBadgeInModal();
|
|
|
| } catch (e) {
|
| console.error("[Rotation Library] Failed to load rotation matrix configuration:", e);
|
| }
|
| }
|
|
|
| function bindModalTabs() {
|
| const tabs = document.querySelectorAll('.rot-tab-btn');
|
| tabs.forEach(tab => {
|
| tab.addEventListener('click', () => {
|
| tabs.forEach(t => t.classList.remove('active'));
|
| tab.classList.add('active');
|
|
|
| const targetPage = tab.dataset.page;
|
| document.querySelectorAll('.rot-modal-page').forEach(page => {
|
| page.style.display = page.id === targetPage ? 'block' : 'none';
|
| });
|
|
|
| if (targetPage === 'rot-page-manage') {
|
| renderPreviousPostsGrid();
|
| } else if (targetPage === 'rot-page-evaluation') {
|
| renderEvaluationDashboard();
|
| }
|
| });
|
| });
|
| }
|
|
|
| |
| |
| |
|
|
| function showPreviousPostsModal() {
|
| const inlinePanel = document.getElementById('previous-posts-inline-panel');
|
| if (inlinePanel) {
|
| inlinePanel.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| inlinePanel.style.outline = '3px solid #0d9488';
|
| inlinePanel.style.borderRadius = 'var(--radius-lg)';
|
| setTimeout(() => {
|
| inlinePanel.style.transition = 'outline 0.8s ease';
|
| inlinePanel.style.outline = '3px solid transparent';
|
| setTimeout(() => {
|
| inlinePanel.style.outline = 'none';
|
| inlinePanel.style.transition = 'none';
|
| }, 800);
|
| }, 1200);
|
| }
|
|
|
| const defaultTab = document.querySelector('.rot-tab-btn[data-page="rot-page-manage"]');
|
| if (defaultTab) defaultTab.click();
|
|
|
| renderPreviousPostsGrid();
|
| applyPreviousPostsSettingsToUI();
|
| }
|
|
|
| function hidePreviousPostsModal() {
|
| console.log("[Rotation Library] Inline workspace mode active. Separation modal skipped.");
|
| }
|
|
|
| async function applyPreviousPostsSettingsToUI() {
|
| const enableRotationChk = document.getElementById('chk-enable-rotation');
|
| const modeSelect = document.getElementById('sel-rotation-mode');
|
| const details = document.getElementById('rotation-config-details');
|
|
|
| if (enableRotationChk) enableRotationChk.checked = isRotationActive;
|
|
|
| const cachedMode = await FritreeStorage.get('local_rotation_mode', 'balanced');
|
| if (modeSelect) modeSelect.value = cachedMode;
|
| if (details) details.style.display = isRotationActive ? 'grid' : 'none';
|
|
|
| updateRotationBadgeInModal();
|
| }
|
|
|
| function updateRotationBadgeInModal() {
|
| const enableRotationChk = document.getElementById('chk-enable-rotation');
|
| const badge = document.getElementById('rotation-status-badge');
|
| if (!badge || !enableRotationChk) return;
|
|
|
| if (enableRotationChk.checked) {
|
| badge.className = "badge green";
|
| badge.innerHTML = '<i class="fa-solid fa-circle-check"></i>Automated Content Rotation Active';
|
| } else {
|
| badge.className = "badge red";
|
| badge.innerHTML = '<i class="fa-solid fa-ban"></i>Automated Content Rotation Disabled';
|
| }
|
| }
|
|
|
| |
| |
|
|
| async function saveAndApplyRotationSettings() {
|
| const enableRotationChk = document.getElementById('chk-enable-rotation');
|
| const modeSelect = document.getElementById('sel-rotation-mode');
|
|
|
| isRotationActive = enableRotationChk ? enableRotationChk.checked : false;
|
| const mode = modeSelect ? modeSelect.value : 'balanced';
|
|
|
| await FritreeStorage.set('local_is_rotation_active', isRotationActive ? 'true' : 'false');
|
| await FritreeStorage.set('local_rotation_mode', mode);
|
|
|
| updateRotationIndicatorUI();
|
|
|
| if (typeof window.addLog === 'function') {
|
| window.addLog('Rotation configuration updated and synchronized successfully.', 'success');
|
| }
|
| }
|
|
|
| async function updateRotationIndicatorUI() {
|
| const indicatorPanel = document.getElementById('prev-posts-indicator-panel');
|
| const composerPanel = document.getElementById('default-composer-panel');
|
| const postTextarea = document.getElementById('post-text');
|
|
|
| if (isRotationActive) {
|
| if (indicatorPanel) indicatorPanel.style.display = 'block';
|
| if (composerPanel) composerPanel.style.opacity = '0.4';
|
| if (postTextarea) postTextarea.disabled = true;
|
|
|
| const activeCount = previousPostsData.filter(p => selectedPrevPostIds.has(p.id) && p.status === 'active').length;
|
| const countLabel = document.getElementById('lbl-active-prev-posts-count');
|
| if (countLabel) countLabel.textContent = activeCount.toLocaleString('en-US');
|
|
|
| const rotationMode = await FritreeStorage.get('local_rotation_mode', 'balanced');
|
| const modeLabel = document.getElementById('lbl-active-prev-posts-mode');
|
|
|
| let modeDisplay = 'Balanced Queue';
|
| if (rotationMode === 'weighted') modeDisplay = 'Priority-Weighted Distribution';
|
| if (rotationMode === 'high_perf') modeDisplay = 'Performance Focus';
|
| if (rotationMode === 'newest') modeDisplay = 'Chronological Order (Newest)';
|
| if (rotationMode === 'oldest') modeDisplay = 'Chronological Order (Oldest)';
|
| if (rotationMode === 'random') modeDisplay = 'Random Dispatch Mode';
|
|
|
| if (modeLabel) modeLabel.textContent = modeDisplay;
|
| } else {
|
| if (indicatorPanel) indicatorPanel.style.display = 'none';
|
| if (composerPanel) composerPanel.style.opacity = '1';
|
| if (postTextarea) postTextarea.disabled = false;
|
| }
|
| }
|
|
|
| async function disableRotationModeGlobally() {
|
| isRotationActive = false;
|
| await FritreeStorage.set('local_is_rotation_active', 'false');
|
| updateRotationIndicatorUI();
|
| applyPreviousPostsSettingsToUI();
|
| if (typeof window.addLog === 'function') {
|
| window.addLog('Automated content rotation disabled. Standard composer controls restored.', 'warn');
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| function selectNextRotationPost(mode, availableIds) {
|
| if (!availableIds || availableIds.length === 0) return null;
|
|
|
| const candidates = previousPostsData.filter(p => availableIds.includes(p.id) && p.status === 'active');
|
| if (candidates.length === 0) return null;
|
|
|
| switch (mode) {
|
| case 'newest':
|
| return [...candidates].sort((a, b) => new Date(b.entryDate || b.creationDate) - new Date(a.entryDate || a.creationDate))[0];
|
|
|
| case 'oldest':
|
| return [...candidates].sort((a, b) => new Date(a.entryDate || a.creationDate) - new Date(b.entryDate || b.creationDate))[0];
|
|
|
| case 'high_perf':
|
| return [...candidates].sort((a, b) => (b.score || 0) - (a.score || 0))[0];
|
|
|
| case 'weighted':
|
| let totalPriority = candidates.reduce((acc, curr) => acc + (parseInt(curr.priority) || 50), 0);
|
| let thresh = Math.random() * totalPriority;
|
| let cumulative = 0;
|
| for (let c of candidates) {
|
| cumulative += (parseInt(c.priority) || 50);
|
| if (thresh <= cumulative) return c;
|
| }
|
| return candidates[0];
|
|
|
| case 'balanced':
|
| return [...candidates].sort((a, b) => {
|
| const usageA = a.stats ? (a.stats.usage || 0) : (a.usageCount || 0);
|
| const usageB = b.stats ? (b.stats.usage || 0) : (b.usageCount || 0);
|
| return usageA - usageB;
|
| })[0];
|
|
|
| case 'random':
|
| default:
|
| return candidates[Math.floor(Math.random() * candidates.length)];
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| function recalculateAllPostScores() {
|
| if (previousPostsData.length === 0) return;
|
|
|
| const metrics = previousPostsData.map(p => {
|
| const usage = p.stats ? (p.stats.usage || 0) : (p.usageCount || 0);
|
| const successCount = p.stats ? ((p.stats.success || 0) + (p.stats.pending || 0)) : (p.usageCount || 0);
|
| const ratio = usage > 0 ? (successCount / usage) : 0;
|
| return { id: p.id, ratio, usage };
|
| });
|
|
|
| const maxUsage = Math.max(...metrics.map(m => m.usage), 1);
|
|
|
| previousPostsData.forEach(p => {
|
| const metric = metrics.find(m => m.id === p.id);
|
| if (!p.stats) {
|
| p.stats = { usage: p.usageCount || 0, success: p.usageCount || 0, pending: 0, failed: 0 };
|
| }
|
|
|
| const successWeightedPoints = (metric ? metric.ratio : 0) * 70;
|
| const consistencyWeightedPoints = (metric ? (metric.usage / maxUsage) : 0) * 30;
|
|
|
| p.score = Math.min(100, Math.max(0, Math.round(successWeightedPoints + consistencyWeightedPoints)));
|
| });
|
| }
|
|
|
| function renderEvaluationDashboard() {
|
| const container = document.getElementById('rot-evaluation-list');
|
| const paginationContainer = document.getElementById('rot-evaluation-pagination');
|
| if (!container) return;
|
|
|
| container.innerHTML = '';
|
| recalculateAllPostScores();
|
|
|
| const sorted = [...previousPostsData].sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
|
| if (previousPostsData.length === 0) {
|
| container.innerHTML = '<tr><td colspan="5" style="text-align:center; padding:20px; color:var(--text-muted);"><i class="fa-solid fa-triangle-exclamation"></i>Database archive is currently empty. Add rotation templates to start accumulating performance analytics.</td></tr>';
|
| if (paginationContainer) paginationContainer.innerHTML = '';
|
| updateEvaluationTotalsRow(sorted);
|
| return;
|
| }
|
|
|
| const startIndex = (currentMetricsPage - 1) * METRICS_ITEMS_PER_PAGE;
|
| const endIndex = startIndex + METRICS_ITEMS_PER_PAGE;
|
| const paginatedItems = sorted.slice(startIndex, endIndex);
|
|
|
| paginatedItems.forEach(p => {
|
| const tr = document.createElement('tr');
|
| tr.style.cursor = 'pointer';
|
| tr.title = "Click to inspect active trace logs and proof links associated with this template.";
|
|
|
| tr.addEventListener('click', () => {
|
| openVariantTraceModal(p.id);
|
| });
|
|
|
| const textIcon = getIconForTemplateText(p.text, p.categories);
|
|
|
| const tdTitle = document.createElement('td');
|
| tdTitle.style.fontWeight = 'bold';
|
| tdTitle.innerHTML = `${textIcon}${p.title || p.text.substring(0, 20) + '...'}`;
|
|
|
| const tdUsage = document.createElement('td');
|
| tdUsage.innerHTML = `<i class="fa-solid fa-play" style="color: #64748b; margin-right: 5px;"></i>${p.stats ? (p.stats.usage || 0).toLocaleString('en-US') : (p.usageCount || 0).toLocaleString('en-US')}`;
|
|
|
| const tdSuccess = document.createElement('td');
|
| tdSuccess.style.color = 'var(--success)';
|
| tdSuccess.innerHTML = `<i class="fa-solid fa-circle-check" style="margin-right: 5px;"></i>${p.stats ? (p.stats.success || 0).toLocaleString('en-US') : (p.usageCount || 0).toLocaleString('en-US')}`;
|
|
|
| const tdPending = document.createElement('td');
|
| tdPending.style.color = 'var(--warning)';
|
| tdPending.innerHTML = `<i class="fa-solid fa-clock" style="margin-right: 5px;"></i>${p.stats ? (p.stats.pending || 0).toLocaleString('en-US') : 0}`;
|
|
|
| const tdFailed = document.createElement('td');
|
| tdFailed.style.color = 'var(--danger)';
|
| tdFailed.innerHTML = `<i class="fa-solid fa-triangle-exclamation" style="margin-right: 5px;"></i>${p.stats ? (p.stats.failed || 0).toLocaleString('en-US') : 0}`;
|
|
|
| tr.appendChild(tdTitle);
|
| tr.appendChild(tdUsage);
|
| tr.appendChild(tdSuccess);
|
| tr.appendChild(tdPending);
|
| tr.appendChild(tdFailed);
|
| container.appendChild(tr);
|
| });
|
|
|
| updateEvaluationTotalsRow(sorted);
|
| renderEvaluationPagination(sorted.length, paginationContainer);
|
| }
|
|
|
| function updateEvaluationTotalsRow(items) {
|
| let publishedSum = 0;
|
| let successfulSum = 0;
|
| let reviewedSum = 0;
|
| let failedSum = 0;
|
|
|
| items.forEach(p => {
|
| if (p.stats) {
|
| publishedSum += (p.stats.usage || 0);
|
| successfulSum += (p.stats.success || 0);
|
| reviewedSum += (p.stats.pending || 0);
|
| failedSum += (p.stats.failed || 0);
|
| }
|
| });
|
|
|
| const totalPubEl = document.getElementById('rot-total-published');
|
| const totalSuccEl = document.getElementById('rot-total-successful');
|
| const totalRevEl = document.getElementById('rot-total-reviewed');
|
| const totalFailEl = document.getElementById('rot-total-failed');
|
|
|
| if (totalPubEl) totalPubEl.innerHTML = `<i class="fa-solid fa-play" style="margin-right:5px;"></i>${publishedSum.toLocaleString('en-US')}`;
|
| if (totalSuccEl) totalSuccEl.innerHTML = `<i class="fa-solid fa-circle-check" style="margin-right:5px;"></i>${successfulSum.toLocaleString('en-US')}`;
|
| if (totalRevEl) totalRevEl.innerHTML = `<i class="fa-solid fa-hourglass-half" style="margin-right:5px;"></i>${reviewedSum.toLocaleString('en-US')}`;
|
| if (totalFailEl) totalFailEl.innerHTML = `<i class="fa-solid fa-triangle-exclamation" style="margin-right:5px;"></i>${failedSum.toLocaleString('en-US')}`;
|
| }
|
|
|
| function renderEvaluationPagination(totalItems, container) {
|
| if (!container) return;
|
| container.innerHTML = '';
|
|
|
| const totalPages = Math.ceil(totalItems / METRICS_ITEMS_PER_PAGE);
|
| if (totalPages <= 1) return;
|
|
|
| const prevBtn = document.createElement('button');
|
| prevBtn.className = 'btn-secondary';
|
| prevBtn.style.padding = '4px 10px';
|
| prevBtn.innerHTML = '<i class="fa-solid fa-chevron-left"></i>';
|
| prevBtn.disabled = currentMetricsPage === 1;
|
| prevBtn.addEventListener('click', () => {
|
| if (currentMetricsPage > 1) {
|
| currentMetricsPage--;
|
| renderEvaluationDashboard();
|
| }
|
| });
|
| container.appendChild(prevBtn);
|
|
|
| const pageIndicator = document.createElement('span');
|
| pageIndicator.style.cssText = 'font-weight:bold; font-size:12px; align-self:center; margin: 0 10px; color:#64748b;';
|
| pageIndicator.textContent = `Page ${currentMetricsPage.toLocaleString('en-US')} of ${totalPages.toLocaleString('en-US')}`;
|
| container.appendChild(pageIndicator);
|
|
|
| const nextBtn = document.createElement('button');
|
| nextBtn.className = 'btn-secondary';
|
| nextBtn.style.padding = '4px 10px';
|
| nextBtn.innerHTML = '<i class="fa-solid fa-chevron-right"></i>';
|
| nextBtn.disabled = currentMetricsPage === totalPages;
|
| nextBtn.addEventListener('click', () => {
|
| if (currentMetricsPage < totalPages) {
|
| currentMetricsPage++;
|
| renderEvaluationDashboard();
|
| }
|
| });
|
| container.appendChild(nextBtn);
|
| }
|
|
|
|
|
|
|
|
|
| function spintaxValidatorAddPost() {
|
| const textVal = document.getElementById('add-prev-post-text').value;
|
| const outputEl = document.getElementById('add-spintax-analysis-output');
|
| if (!outputEl) return;
|
|
|
| if (!textVal) {
|
| outputEl.innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i> Error: Message text copy is empty! Please write spintax content first.`;
|
| outputEl.style.color = "var(--danger)";
|
| return;
|
| }
|
|
|
| if (typeof FritreeRules !== 'undefined' && typeof FritreeRules.analyzeSpintax === 'function') {
|
| const analysis = FritreeRules.analyzeSpintax(textVal);
|
| if (!analysis.valid && analysis.errors) {
|
| outputEl.innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i> ${analysis.errors}`;
|
| outputEl.style.color = "var(--danger)";
|
| } else {
|
| outputEl.innerHTML = `<i class="fa-solid fa-circle-check"></i>Spintax brackets verified successfully. <strong>Expected variations: ${analysis.variations.toLocaleString('en-US')} unique copy paths.</strong>`;
|
| outputEl.style.color = "var(--success)";
|
| }
|
| } else {
|
| outputEl.innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i> Spintax analysis engine is temporarily offline.`;
|
| }
|
| }
|
|
|
| function handleAddPrevPostFiles(files) {
|
| for (let i = 0; i < files.length; i++) {
|
| const file = files[i];
|
| if (file.size > 25 * 1024 * 1024) {
|
| alert(`File ${file.name} is too large! Maximum allowed upload size is 25MB.`);
|
| continue;
|
| }
|
| if (!file.type.startsWith('image/')) continue;
|
|
|
| const reader = new FileReader();
|
| reader.onload = async () => {
|
| const compressedBlob = typeof FritreeLibrary !== 'undefined' ?
|
| await FritreeLibrary.compressImageBlob(file) : file;
|
| const thumbBlob = typeof FritreeLibrary !== 'undefined' ?
|
| await FritreeLibrary.generateImageThumbnail(compressedBlob) : file;
|
|
|
| const binaryHash = 'rot_media_add_' + Date.now() + "_" + Math.random().toString(36).substr(2,4);
|
|
|
| await FritreeStorage.set(`media_blob_${binaryHash}`, compressedBlob);
|
| await FritreeStorage.set(`media_thumb_${binaryHash}`, thumbBlob);
|
|
|
| addPrevPostMedia.push({
|
| id: binaryHash,
|
| type: "image/jpeg",
|
| blob: compressedBlob,
|
| thumbBlob: thumbBlob,
|
| filename: file.name
|
| });
|
| renderAddPrevPostPreviews();
|
| };
|
| reader.readAsDataURL(file);
|
| }
|
| const addFileInput = document.getElementById('add-prev-post-file-input');
|
| if (addFileInput) addFileInput.value = '';
|
| }
|
|
|
| function renderAddPrevPostPreviews() {
|
| const container = document.getElementById('add-prev-post-previews');
|
| if (!container) return;
|
| container.innerHTML = '';
|
|
|
| for (let i = 0; i < addPrevPostMedia.length; i++) {
|
| const media = addPrevPostMedia[i];
|
| const box = document.createElement('div');
|
| box.className = 'preview-box';
|
|
|
| const el = document.createElement('img');
|
| const previewUrl = URL.createObjectURL(media.thumbBlob || media.blob);
|
| el.src = previewUrl;
|
| box.dataset.objurl = previewUrl;
|
| el.style.cssText = 'width: 100%; height: 100%; object-fit: cover;';
|
|
|
| const closeBtn = document.createElement('button');
|
| closeBtn.setAttribute('type', 'button');
|
| closeBtn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
| closeBtn.addEventListener('click', () => {
|
| if (box.dataset.objurl) URL.revokeObjectURL(box.dataset.objurl);
|
| FritreeStorage.remove(`media_blob_${media.id}`);
|
| FritreeStorage.remove(`media_thumb_${media.id}`);
|
| addPrevPostMedia.splice(i, 1);
|
| renderAddPrevPostPreviews();
|
| });
|
|
|
| box.appendChild(el);
|
| box.appendChild(closeBtn);
|
| container.appendChild(box);
|
| }
|
| }
|
|
|
| async function handleAddNewRotationPost() {
|
| const titleVal = document.getElementById('add-prev-post-title').value.trim();
|
| const textVal = document.getElementById('add-prev-post-text').value;
|
| const statusVal = document.getElementById('add-prev-post-status').value;
|
| const keywordsVal = document.getElementById('add-prev-post-keywords').value;
|
| const phonesVal = document.getElementById('add-prev-post-phones').value;
|
| const noteVal = document.getElementById('add-prev-post-notes').value.trim();
|
| const categoryVal = document.getElementById('add-prev-post-categories').value;
|
|
|
| const altTextVal = document.getElementById('add-prev-post-alt-text').value;
|
| const priorityVal = parseInt(document.getElementById('add-prev-post-priority').value) || 50;
|
|
|
| const entryDateVal = document.getElementById('add-prev-post-entry-date').value;
|
| const expireDateVal = document.getElementById('add-prev-post-expire-date').value;
|
| const clientNameVal = document.getElementById('add-prev-post-client-name').value.trim();
|
|
|
| const enableWaFollowUp = document.getElementById('add-prev-post-wa-followup').checked;
|
| const followUpMessage = document.getElementById('add-prev-post-followup-msg').value;
|
| const followUpDate = document.getElementById('add-prev-post-followup-date').value;
|
|
|
| if (!textVal && addPrevPostMedia.length === 0) {
|
| alert('Save aborted: Cannot record a blank template block. Provide a text copy or attach media files.');
|
| return;
|
| }
|
|
|
| const keywordsArray = keywordsVal.split(',').map(s => s.trim().toLowerCase()).filter(s => s);
|
| const phonesArray = phonesVal.split(',').map(s => s.trim().replace(/[^0-9+]/g, '')).filter(s => s);
|
| const categoryArray = categoryVal.split(',').map(s => s.trim()).filter(s => s);
|
|
|
| const newPost = {
|
| id: 'rot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4),
|
| title: titleVal || (textVal ? textVal.substring(0, 30) + '...' : 'Manual Archived Dispatch'),
|
| text: textVal,
|
| altText: altTextVal,
|
| priority: priorityVal,
|
| mediaReferences: addPrevPostMedia.map(m => ({ id: m.id, type: m.type, filename: m.filename })),
|
| status: statusVal,
|
| keywords: keywordsArray,
|
| categories: categoryArray,
|
| notes: noteVal,
|
| enableWaFollowUp: enableWaFollowUp,
|
| clientName: clientNameVal,
|
| phoneNumbers: phonesArray,
|
| followUpDate: followUpDate,
|
| followUpMessage: followUpMessage,
|
| entryDate: entryDateVal,
|
| expireDate: expireDateVal,
|
| followUpSent: false,
|
| date: new Date().toISOString()
|
| };
|
|
|
| previousPostsData.unshift(newPost);
|
| await savePrevPostsToLocal();
|
|
|
| document.getElementById('add-prev-post-title').value = '';
|
| document.getElementById('add-prev-post-text').value = '';
|
| document.getElementById('add-prev-post-keywords').value = '';
|
| document.getElementById('add-prev-post-phones').value = '';
|
| document.getElementById('add-prev-post-notes').value = '';
|
| document.getElementById('add-prev-post-categories').value = '';
|
| document.getElementById('add-prev-post-entry-date').value = '';
|
| document.getElementById('add-prev-post-expire-date').value = '';
|
| document.getElementById('add-prev-post-client-name').value = '';
|
| document.getElementById('add-prev-post-wa-followup').checked = false;
|
| document.getElementById('add-prev-post-followup-msg').value = '';
|
| document.getElementById('add-prev-post-followup-date').value = '';
|
| document.getElementById('add-prev-post-alt-text').value = '';
|
| document.getElementById('add-prev-post-priority').value = '50';
|
|
|
| addPrevPostMedia = [];
|
| const previewsContainer = document.getElementById('add-prev-post-previews');
|
| if (previewsContainer) previewsContainer.innerHTML = '';
|
|
|
| triggerOnScreenCelebration();
|
|
|
| const manageTab = document.querySelector('.rot-tab-btn[data-page="rot-page-manage"]');
|
| if (manageTab) manageTab.click();
|
|
|
| if (typeof window.addLog === 'function') {
|
| window.addLog(`Successfully saved new rotation template block: "${newPost.title}".`, 'success');
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| function renderPreviousPostsGrid() {
|
| const container = document.getElementById('prev-posts-grid-container');
|
| if (!container) return;
|
| container.innerHTML = '';
|
|
|
| const queryInput = document.getElementById('prev-posts-search');
|
| const statusSelect = document.getElementById('prev-posts-filter-status');
|
| const sortSelect = document.getElementById('prev-posts-sort-by');
|
|
|
| const keywordInput = document.getElementById('prev-posts-filter-keyword');
|
| const categoryInput = document.getElementById('prev-posts-filter-category');
|
| const notesInput = document.getElementById('prev-posts-filter-notes');
|
| const followupDateInput = document.getElementById('prev-posts-filter-followup-date');
|
| const minScoreInput = document.getElementById('prev-posts-filter-min-score');
|
| const maxCharsInput = document.getElementById('prev-posts-filter-max-chars');
|
|
|
| const query = queryInput ? queryInput.value.toLowerCase().trim() : '';
|
| const statusFilter = statusSelect ? statusSelect.value : 'all';
|
| const sortBy = sortSelect ? sortSelect.value : 'newest';
|
|
|
| const keywordFilter = keywordInput ? keywordInput.value.toLowerCase().trim() : '';
|
| const categoryFilter = categoryInput ? categoryInput.value.toLowerCase().trim() : '';
|
| const notesFilter = notesInput ? notesInput.value.toLowerCase().trim() : '';
|
| const followupDateFilter = followupDateInput ? followupDateInput.value : '';
|
| const minScoreFilter = minScoreInput ? parseInt(minScoreInput.value) || 0 : 0;
|
| const maxCharsFilter = maxCharsInput ? parseInt(maxCharsInput.value) || 0 : 0;
|
|
|
| const filtered = previousPostsData.filter(p => {
|
| const textContent = p.text || '';
|
| const matchSearch = textContent.toLowerCase().includes(query) || (p.title && p.title.toLowerCase().includes(query));
|
|
|
| let matchStatus = true;
|
| if (statusFilter !== 'all') {
|
| matchStatus = p.status === statusFilter;
|
| }
|
|
|
| let matchKeyword = true;
|
| if (keywordFilter) {
|
| matchKeyword = p.keywords?.some(k => k.toLowerCase().includes(keywordFilter));
|
| }
|
|
|
| let matchCategory = true;
|
| if (categoryFilter) {
|
| matchCategory = p.categories?.some(c => c.toLowerCase().includes(categoryFilter));
|
| }
|
|
|
| let matchNotes = true;
|
| if (notesFilter) {
|
| matchNotes = p.teamNotes?.toLowerCase().includes(notesFilter);
|
| }
|
|
|
| let matchFollowUpDate = true;
|
| if (followupDateFilter) {
|
| matchFollowUpDate = p.followUpDate === followupDateFilter;
|
| }
|
|
|
| let matchMinScore = true;
|
| if (minScoreFilter) {
|
| matchMinScore = (p.score || 0) >= minScoreFilter;
|
| }
|
|
|
| let matchMaxChars = true;
|
| if (maxCharsFilter) {
|
| matchMaxChars = textContent.length <= maxCharsFilter;
|
| }
|
|
|
| return matchSearch && matchStatus && matchKeyword && matchCategory && matchNotes && matchFollowUpDate && matchMinScore && matchMaxChars;
|
| });
|
|
|
| if (sortBy === 'newest') filtered.sort((a,b) => new Date(b.creationDate || b.entryDate) - new Date(a.creationDate || a.entryDate));
|
| else if (sortBy === 'oldest') filtered.sort((a,b) => new Date(a.creationDate || a.entryDate) - new Date(b.creationDate || b.entryDate));
|
| else if (sortBy === 'most_pub') filtered.sort((a,b) => (b.stats?.usage || 0) - (a.stats?.usage || 0));
|
| else if (sortBy === 'least_pub') filtered.sort((a,b) => (a.stats?.usage || 0) - (b.stats?.usage || 0));
|
| else if (sortBy === 'highest_score') filtered.sort((a,b) => (b.score || 0) - (a.score || 0));
|
| else if (sortBy === 'lowest_score') filtered.sort((a,b) => (a.score || 0) - (b.score || 0));
|
|
|
| if (filtered.length === 0) {
|
| const emptyEl = document.createElement('div');
|
| emptyEl.style.cssText = 'text-align: center; color: #64748b; padding: 20px; font-size: 12px;';
|
| emptyEl.innerHTML = '<i class="fa-solid fa-folder-open"></i> Rotation matrix is currently empty or contains no template blocks matching active filters.';
|
| container.appendChild(emptyEl);
|
| return;
|
| }
|
|
|
| filtered.forEach(p => {
|
| const item = document.createElement('div');
|
| item.style.cssText = 'display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px; background: white; border: 1px solid #e2e8f0; border-radius: 15px; transition: none; direction: ltr; text-align: left;';
|
|
|
| const chk = document.createElement('input');
|
| chk.type = 'checkbox';
|
| chk.className = 'prev-post-item-checkbox';
|
| chk.value = p.id;
|
| chk.checked = selectedPrevPostIds.has(p.id);
|
| chk.style.cssText = 'width: 18px; height: 18px; cursor: pointer; accent-color: var(--primary); flex-shrink: 0;';
|
|
|
| if (chk.checked) item.style.borderColor = '#93c5fd';
|
|
|
| chk.addEventListener('change', async () => {
|
| if (chk.checked) {
|
| selectedPrevPostIds.add(p.id);
|
| item.style.borderColor = '#93c5fd';
|
| } else {
|
| selectedPrevPostIds.delete(p.id);
|
| item.style.borderColor = '#e2e8f0';
|
| }
|
| await FritreeStorage.set('local_selected_prev_posts', Array.from(selectedPrevPostIds));
|
| updateRotationIndicatorUI();
|
| });
|
|
|
| const contentDiv = document.createElement('div');
|
| contentDiv.style.cssText = 'flex: 1; display: flex; flex-direction: column; gap: 4px; overflow: hidden; padding-left: 10px; text-align: left;';
|
|
|
| const textIcon = getIconForTemplateText(p.text, p.categories);
|
|
|
| const titleSpan = document.createElement('span');
|
| titleSpan.style.cssText = 'font-weight: bold; font-size: 13px; color: #1e293b; text-align: left; display: flex; align-items: center; gap: 6px; flex-wrap: wrap;';
|
| titleSpan.innerHTML = `${textIcon} ${p.title || 'Manual Archived Dispatch'}`;
|
|
|
| const statBadge = document.createElement('span');
|
| let statColor = 'background: #f1f5f9; color: #475569;';
|
| let statText = 'Draft';
|
|
|
| if (p.status === 'active') { statColor = 'background: #dcfce7; color: #166534;'; statText = '<i class="fa-solid fa-circle-check"></i>Active'; }
|
| else if (p.status === 'frozen') { statColor = 'background: #e0f2fe; color: #0369a1;'; statText = '<i class="fa-solid fa-snowflake"></i>Frozen'; }
|
| else if (p.status === 'draft') { statColor = 'background: #f1f5f9; color: #475569;'; statText = '<i class="fa-solid fa-file-pen"></i>Draft'; }
|
| else if (p.status === 'inactive') { statColor = 'background: #fee2e2; color: #991b1b;'; statText = '<i class="fa-solid fa-circle-xmark"></i>Deactivated'; }
|
|
|
| statBadge.style.cssText = `${statColor} padding: 2px 6px; border-radius: 4px; font-size: 9px; font-weight: bold; text-transform: uppercase; margin-left: 5px; display: inline-flex; align-items: center; gap: 3px;`;
|
| statBadge.innerHTML = statText;
|
| titleSpan.appendChild(statBadge);
|
|
|
| if (p.enableWaFollowUp && p.followUpDate) {
|
| const fuBadge = document.createElement('span');
|
| fuBadge.style.cssText = 'background: #fdf2f8; color: #9d174d; padding: 2px 6px; border-radius: 4px; font-size: 9px; font-weight: bold; margin-left: 5px;';
|
| fuBadge.innerHTML = `<i class="fa-brands fa-whatsapp"></i>Follow-Up: ${p.followUpDate}`;
|
| titleSpan.appendChild(fuBadge);
|
| }
|
|
|
| const previewText = document.createElement('span');
|
| previewText.style.cssText = 'font-size: 12px; color: #64748b; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; max-width: 350px; text-align: left; display: block;';
|
| previewText.textContent = p.text || '[Media Attachment Assets Only]';
|
|
|
| const statsRow = document.createElement('div');
|
| statsRow.style.cssText = 'display: flex; gap: 12px; font-size: 11px; color: #94a3b8; margin-top: 2px; flex-wrap: wrap; justify-content: flex-start;';
|
|
|
| const usageSpan = document.createElement('span');
|
| const useCount = p.stats ? (p.stats.usage || 0) : (p.usageCount || 0);
|
| usageSpan.innerHTML = `<i class="fa-solid fa-play" style="margin-right: 3px;"></i>Dispatched: ${useCount.toLocaleString('en-US')} times`;
|
|
|
| const dateSpan = document.createElement('span');
|
| const dateVal = p.entryDate || p.creationDate;
|
| dateSpan.innerHTML = `<i class="fa-regular fa-calendar" style="margin-right: 3px;"></i>Created: ${new Date(dateVal).toLocaleDateString('en-US')}`;
|
|
|
| statsRow.appendChild(usageSpan);
|
| statsRow.appendChild(dateSpan);
|
|
|
| if (p.categories && p.categories.length > 0) {
|
| const catSpan = document.createElement('span');
|
| catSpan.innerHTML = `<i class="fa-solid fa-tags" style="margin-right: 3px;"></i>${p.categories.join(', ')}`;
|
| statsRow.appendChild(catSpan);
|
| }
|
|
|
| contentDiv.appendChild(titleSpan);
|
| contentDiv.appendChild(previewText);
|
| contentDiv.appendChild(statsRow);
|
|
|
| const mediaContainer = document.createElement('div');
|
| mediaContainer.style.cssText = 'display: flex; gap: 4px; align-items: center; justify-content: center; min-width: 60px;';
|
|
|
| if (p.mediaReferences && p.mediaReferences.length > 0) {
|
| p.mediaReferences.slice(0, 2).forEach(async m => {
|
| const thumbBlob = await FritreeStorage.get(`media_thumb_${m.id}`);
|
| if (thumbBlob) {
|
| const img = document.createElement('img');
|
| img.src = URL.createObjectURL(thumbBlob);
|
| img.style.cssText = 'width: 32px; height: 32px; object-fit: cover; border-radius: 6px; border: 1px solid #cbd5e1;';
|
| mediaContainer.appendChild(img);
|
| }
|
| });
|
| } else {
|
| const noMedia = document.createElement('span');
|
| noMedia.style.cssText = 'font-size: 20px; color: #e2e8f0;';
|
| noMedia.innerHTML = '<i class="fa-solid fa-file-lines"></i>';
|
| mediaContainer.appendChild(noMedia);
|
| }
|
|
|
| const tdActs = document.createElement('div');
|
| tdActs.style.cssText = 'display: flex; gap: 4px; align-items: center;';
|
|
|
| const editBtn = document.createElement('button');
|
| editBtn.className = 'action-btn view';
|
| editBtn.title = "Modify Template Parameters";
|
| editBtn.innerHTML = '<i class="fa-solid fa-pen-to-square"></i>';
|
| editBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| openEditPrevPostModal(p.id);
|
| });
|
|
|
| const delBtn = document.createElement('button');
|
| delBtn.className = 'action-btn delete';
|
| delBtn.title = "Wipe Template Permanently";
|
| delBtn.innerHTML = '<i class="fa-solid fa-trash"></i>';
|
| delBtn.addEventListener('click', (e) => {
|
| e.stopPropagation();
|
| if (confirm('Are you sure you want to permanently delete this rotation template block?')) {
|
| if (p.mediaReferences) {
|
| p.mediaReferences.forEach(m => {
|
| FritreeStorage.remove(`media_blob_${m.id}`);
|
| FritreeStorage.remove(`media_thumb_${m.id}`);
|
| });
|
| }
|
| previousPostsData = previousPostsData.filter(x => x.id !== p.id);
|
| selectedPrevPostIds.delete(p.id);
|
| savePrevPostsToLocal();
|
| renderPreviousPostsGrid();
|
| updateRotationIndicatorUI();
|
| }
|
| });
|
|
|
| tdActs.appendChild(editBtn);
|
| tdActs.appendChild(delBtn);
|
|
|
| item.appendChild(chk);
|
| item.appendChild(contentDiv);
|
| item.appendChild(mediaContainer);
|
| item.appendChild(tdActs);
|
| container.appendChild(item);
|
| });
|
| }
|
|
|
| async function bulkPrevPostsAction(action) {
|
| const container = document.getElementById('prev-posts-grid-container');
|
| if (!container) return;
|
| const checkedBoxes = container.querySelectorAll('.prev-post-item-checkbox:checked');
|
|
|
| if (checkedBoxes.length === 0) {
|
| alert('Please select at least one template block first to perform bulk actions.');
|
| return;
|
| }
|
|
|
| if (action === 'delete') {
|
| if (!confirm(`Are you sure you want to permanently delete ${checkedBoxes.length} selected template block(s)?`)) return;
|
| checkedBoxes.forEach(chk => {
|
| const post = previousPostsData.find(x => x.id === chk.value);
|
| if (post && post.mediaReferences) {
|
| post.mediaReferences.forEach(m => {
|
| FritreeStorage.remove(`media_blob_${chk.value}_${m.id}`);
|
| FritreeStorage.remove(`media_thumb_${chk.value}_${m.id}`);
|
| });
|
| }
|
| previousPostsData = previousPostsData.filter(x => x.id !== chk.value);
|
| selectedPrevPostIds.delete(chk.value);
|
| });
|
| } else {
|
| checkedBoxes.forEach(chk => {
|
| const post = previousPostsData.find(x => x.id === chk.value);
|
| if (post) {
|
| if (action === 'draft') post.status = 'draft';
|
| else if (action === 'active') post.status = 'active';
|
| else if (action === 'inactive') post.status = 'inactive';
|
| else if (action === 'freeze') post.status = 'frozen';
|
| else if (action === 'unfreeze') post.status = 'active';
|
| }
|
| });
|
| }
|
|
|
| await savePrevPostsToLocal();
|
| await FritreeStorage.set('local_selected_prev_posts', Array.from(selectedPrevPostIds));
|
|
|
| renderPreviousPostsGrid();
|
| updateRotationIndicatorUI();
|
|
|
| const selectAllCheck = document.getElementById('prev-posts-select-all');
|
| if (selectAllCheck) selectAllCheck.checked = false;
|
|
|
| if (typeof window.addLog === 'function') {
|
| window.addLog(`Bulk action [${action}] executed successfully.`, 'warn');
|
| }
|
| }
|
|
|
| async function incrementPrevPostUsage(id, status = 'success') {
|
| const post = previousPostsData.find(x => x.id === id);
|
| if (post) {
|
| if (!post.stats) {
|
| post.stats = { usage: 0, success: 0, fail: 0, pending: 0, realInteractions: 0 };
|
| }
|
| post.stats.usage = (post.stats.usage || 0) + 1;
|
| post.usageCount = (post.usageCount || 0) + 1;
|
|
|
| if (status === 'success') post.stats.success++;
|
| else if (status === 'pending') post.stats.pending++;
|
| else if (status === 'failed') post.stats.failed++;
|
|
|
| recalculateAllPostScores();
|
| await savePrevPostsToLocal();
|
|
|
| const inlinePanel = document.getElementById('previous-posts-inline-panel');
|
| if (inlinePanel) {
|
| renderPreviousPostsGrid();
|
| renderEvaluationDashboard();
|
| }
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| async function openEditPrevPostModal(id) {
|
| const post = previousPostsData.find(p => p.id === id);
|
| if (!post) return;
|
|
|
| document.getElementById('edit-prev-post-id').value = post.id;
|
| document.getElementById('edit-prev-post-title').value = post.title || '';
|
| document.getElementById('edit-prev-post-text').value = post.text || '';
|
|
|
| document.getElementById('edit-prev-post-status').value = post.status || 'active';
|
| document.getElementById('edit-prev-post-keywords').value = post.keywords ? post.keywords.join(', ') : '';
|
| document.getElementById('edit-prev-post-phones').value = post.phoneNumbers ? post.phoneNumbers.join(', ') : '';
|
| document.getElementById('edit-prev-post-notes').value = post.teamNotes || '';
|
| document.getElementById('edit-prev-post-categories').value = post.categories ? post.categories.join(', ') : '';
|
|
|
| document.getElementById('edit-prev-post-entry-date').value = post.entryDate ? post.entryDate.slice(0, 16) : '';
|
| document.getElementById('edit-prev-post-expire-date').value = post.expireDate ? post.expireDate.slice(0, 16) : '';
|
| document.getElementById('edit-prev-post-client-name').value = post.clientName || '';
|
|
|
| document.getElementById('edit-prev-post-wa-followup').checked = !!post.enableWaFollowUp;
|
| document.getElementById('edit-prev-post-followup-msg').value = post.followUpMessage || '';
|
| document.getElementById('edit-prev-post-followup-date').value = post.followUpDate || '';
|
|
|
| editPrevPostMedia = [];
|
| if (post.mediaReferences) {
|
| for (let i = 0; i < post.mediaReferences.length; i++) {
|
| const ref = post.mediaReferences[i];
|
| const rawBlob = await FritreeStorage.get(`media_blob_${ref.id}`);
|
| const thumbBlob = await FritreeStorage.get(`media_thumb_${ref.id}`);
|
|
|
| editPrevPostMedia.push({
|
| id: ref.id,
|
| type: ref.type,
|
| blob: rawBlob,
|
| thumbBlob: thumbBlob,
|
| filename: ref.filename
|
| });
|
| }
|
| }
|
| renderEditPrevPostPreviews();
|
|
|
| const modal = document.getElementById('edit-prev-post-modal');
|
| if (modal) modal.style.display = 'flex';
|
| }
|
|
|
| function handleEditPrevPostFiles(files) {
|
| for (let i = 0; i < files.length; i++) {
|
| const file = files[i];
|
| if (file.size > 25 * 1024 * 1024) {
|
| alert(`File ${file.name} is too large! Maximum allowed upload size is 25MB.`);
|
| continue;
|
| }
|
| if (!file.type.startsWith('image/')) continue;
|
|
|
| const reader = new FileReader();
|
| reader.onload = async () => {
|
| const compressedBlob = typeof FritreeLibrary !== 'undefined' ?
|
| await FritreeLibrary.compressImageBlob(file) : file;
|
| const thumbBlob = typeof FritreeLibrary !== 'undefined' ?
|
| await FritreeLibrary.generateImageThumbnail(compressedBlob) : file;
|
|
|
| const binaryHash = 'rot_media_edit_' + Date.now() + "_" + Math.random().toString(36).substr(2,4);
|
|
|
| await FritreeStorage.set(`media_blob_${binaryHash}`, compressedBlob);
|
| await FritreeStorage.set(`media_thumb_${binaryHash}`, thumbBlob);
|
|
|
| editPrevPostMedia.push({
|
| id: binaryHash,
|
| type: "image/jpeg",
|
| blob: compressedBlob,
|
| thumbBlob: thumbBlob,
|
| filename: file.name
|
| });
|
| renderEditPrevPostPreviews();
|
| };
|
| reader.readAsDataURL(file);
|
| }
|
|
|
| const editFileInput = document.getElementById('edit-prev-post-file-input');
|
| if (editFileInput) editFileInput.value = '';
|
| }
|
|
|
| async function renderEditPrevPostPreviews() {
|
| const container = document.getElementById('edit-prev-post-previews');
|
| if (!container) return;
|
| container.innerHTML = '';
|
|
|
| for (let i = 0; i < editPrevPostMedia.length; i++) {
|
| const media = editPrevPostMedia[i];
|
| const box = document.createElement('div');
|
| box.className = 'preview-box';
|
|
|
| const el = document.createElement('img');
|
| if (media.thumbBlob || media.blob) {
|
| const previewUrl = URL.createObjectURL(media.thumbBlob || media.blob);
|
| el.src = previewUrl;
|
| box.dataset.objurl = previewUrl;
|
| } else {
|
| el.src = 'icon.png';
|
| }
|
|
|
| el.style.width = '100%';
|
| el.style.height = '100%';
|
| el.style.objectFit = 'cover';
|
|
|
| const closeBtn = document.createElement('button');
|
| closeBtn.setAttribute('type', 'button');
|
| closeBtn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
| closeBtn.addEventListener('click', () => {
|
| if (box.dataset.objurl) URL.revokeObjectURL(box.dataset.objurl);
|
| FritreeStorage.remove(`media_blob_${media.id}`);
|
| FritreeStorage.remove(`media_thumb_${media.id}`);
|
| editPrevPostMedia.splice(i, 1);
|
| renderEditPrevPostPreviews();
|
| });
|
|
|
| box.appendChild(el);
|
| box.appendChild(closeBtn);
|
| container.appendChild(box);
|
| }
|
| }
|
|
|
| async function saveEditedPreviousPost() {
|
| const id = document.getElementById('edit-prev-post-id').value;
|
| const post = previousPostsData.find(p => p.id === id);
|
| if (!post) return;
|
|
|
| const titleVal = document.getElementById('edit-prev-post-title').value.trim();
|
| const textVal = document.getElementById('edit-prev-post-text').value;
|
|
|
| const statusVal = document.getElementById('edit-prev-post-status').value;
|
| const keywordsVal = document.getElementById('edit-prev-post-keywords').value;
|
| const phonesVal = document.getElementById('edit-prev-post-phones').value;
|
| const noteVal = document.getElementById('edit-prev-post-notes').value.trim();
|
| const categoryVal = document.getElementById('edit-prev-post-categories').value;
|
|
|
| const entryDateVal = document.getElementById('edit-prev-post-entry-date').value;
|
| const expireDateVal = document.getElementById('edit-prev-post-expire-date').value;
|
| const clientNameVal = document.getElementById('edit-prev-post-client-name').value.trim();
|
|
|
| const enableWaFollowUp = document.getElementById('edit-prev-post-wa-followup').checked;
|
| const followUpMessage = document.getElementById('edit-prev-post-followup-msg').value;
|
| const followUpDate = document.getElementById('edit-prev-post-followup-date').value;
|
|
|
| if (!textVal && editPrevPostMedia.length === 0) {
|
| alert('Save aborted: Cannot save a blank template. Provide a text copy or attach media files.');
|
| return;
|
| }
|
|
|
| const keywordsArray = keywordsVal.split(',').map(s => s.trim().toLowerCase()).filter(s => s);
|
| const phonesArray = phonesVal.split(',').map(s => s.trim().replace(/[^0-9+]/g, '')).filter(s => s);
|
| const categoryArray = categoryVal.split(',').map(s => s.trim()).filter(s => s);
|
|
|
| post.title = titleVal || (textVal ? textVal.substring(0, 30) + '...' : 'Manual Archived Dispatch');
|
| post.text = textVal;
|
| post.mediaReferences = editPrevPostMedia.map(m => ({ id: m.id, type: m.type, filename: m.filename }));
|
| post.status = statusVal;
|
| post.keywords = keywordsArray;
|
| post.categories = categoryArray;
|
| post.notes = noteVal;
|
| post.enableWaFollowUp = enableWaFollowUp;
|
| post.clientName = clientNameVal;
|
| post.phoneNumbers = phonesArray;
|
| post.followUpDate = followUpDate;
|
| post.followUpMessage = followUpMessage;
|
| post.entryDate = entryDateVal;
|
| post.expireDate = expireDateVal;
|
| post.followUpSent = false;
|
|
|
| await savePrevPostsToLocal();
|
| renderPreviousPostsGrid();
|
|
|
| document.querySelectorAll('#edit-prev-post-previews .preview-box').forEach(box => {
|
| if (box.dataset.objurl) URL.revokeObjectURL(box.dataset.objurl);
|
| });
|
|
|
| const modal = document.getElementById('edit-prev-post-modal');
|
| if (modal) modal.style.display = 'none';
|
|
|
| if (typeof window.addLog === 'function') {
|
| window.addLog(`Successfully updated rotation template block parameters: "${post.title}".`, 'success');
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| async function handleSaveCurrentToLibraryManually() {
|
| const text = document.getElementById('post-text').value;
|
| const images = window.FritreeFacebook ? await window.FritreeFacebook.getUploadedImages() : [];
|
|
|
| if (!text && images.length === 0) {
|
| alert('Standard composer is empty. Write text or attach media to save as template.');
|
| return;
|
| }
|
|
|
| const title = prompt('Enter a descriptive administrative title to save current composer state as template block:');
|
| if (!title) return;
|
|
|
| await autoSaveCurrentPostToLibrary(text, images, title);
|
| renderPreviousPostsGrid();
|
| }
|
|
|
| async function autoSaveCurrentPostToLibrary(text, images, title = '') {
|
| if (!text && images.length === 0) return;
|
| const summaryTitle = title || (text ? text.substring(0, 25) + '...' : 'Media Only Template');
|
|
|
| const hasDuplicate = previousPostsData.some(p => p.text === text && p.text.length > 0);
|
| if (hasDuplicate && !title) return;
|
|
|
| const newPost = {
|
| id: 'rot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4),
|
| title: sanitizeRotationInput(summaryTitle),
|
| text: text,
|
| mediaReferences: [],
|
| status: 'active',
|
| keywords: [],
|
| phoneNumbers: [],
|
| enableWaFollowUp: false,
|
| followUpMessage: '',
|
| followUpDate: '',
|
| followUpSent: false,
|
| entryDate: new Date().toISOString(),
|
| expireDate: '',
|
| clientName: '',
|
| teamNotes: '',
|
| categories: [],
|
| stats: { usage: 0, success: 0, pending: 0, failed: 0 },
|
| score: 50,
|
| creationDate: new Date().toISOString()
|
| };
|
|
|
| if (images && images.length > 0) {
|
| for (let i = 0; i < images.length; i++) {
|
| const imgObj = images[i];
|
| const binaryHash = 'rot_media_' + Date.now() + "_" + Math.random().toString(36).substr(2,4);
|
|
|
| let rawBlob = null;
|
| if (imgObj.id) {
|
| rawBlob = await FritreeStorage.get(`media_blob_${imgObj.id}`);
|
| } else if (imgObj.data) {
|
| rawBlob = typeof FritreeStorage.b64ToBlob === 'function' ?
|
| FritreeStorage.b64ToBlob(imgObj.data, imgObj.type) : imgObj.data;
|
| }
|
|
|
| if (rawBlob) {
|
| const thumbBlob = typeof FritreeLibrary !== 'undefined' ?
|
| await FritreeLibrary.generateImageThumbnail(rawBlob) : rawBlob;
|
|
|
| await FritreeStorage.set(`media_blob_${binaryHash}`, rawBlob);
|
| await FritreeStorage.set(`media_thumb_${binaryHash}`, thumbBlob);
|
|
|
| newPost.mediaReferences.push({
|
| id: binaryHash,
|
| type: imgObj.type || "image/jpeg",
|
| filename: imgObj.name || "image.jpg"
|
| });
|
| }
|
| }
|
| }
|
|
|
| previousPostsData.unshift(newPost);
|
| recalculateAllPostScores();
|
| await savePrevPostsToLocal();
|
| if (typeof window.addLog === 'function') {
|
| window.addLog(`Composer state automatically archived in Rotation Library under descriptive title: "${newPost.title}".`, 'success');
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| function startRotationActiveSchedulers() {
|
| setInterval(async () => {
|
| await checkAndTriggerFollowUps();
|
| await checkAndProcessExpiredRotationPosts();
|
| }, 30000);
|
| }
|
|
|
| async function checkAndProcessExpiredRotationPosts() {
|
| let changed = false;
|
| const now = Date.now();
|
|
|
| previousPostsData.forEach(p => {
|
| if (p.status === 'active' && p.expireDate) {
|
| const expTime = new Date(p.expireDate).getTime();
|
| if (now >= expTime) {
|
| p.status = 'frozen';
|
| changed = true;
|
| if (typeof window.addLog === 'function') {
|
| window.addLog(`Auto-Freeze Alert: Rotation template block "${p.title}" exceeded its expiration limit and has been deactivated.`, 'warn');
|
| }
|
| }
|
| }
|
| });
|
|
|
| if (changed) {
|
| await savePrevPostsToLocal();
|
| const inlinePanel = document.getElementById('previous-posts-inline-panel');
|
| if (inlinePanel) {
|
| renderPreviousPostsGrid();
|
| renderEvaluationDashboard();
|
| }
|
| }
|
| }
|
|
|
| async function checkAndTriggerFollowUps() {
|
| let changed = false;
|
| const now = Date.now();
|
|
|
| for (let i = 0; i < previousPostsData.length; i++) {
|
| const p = previousPostsData[i];
|
| if (p.status === 'active' && p.enableWaFollowUp && p.followUpDate && !p.followUpSent) {
|
| const fDate = new Date(p.followUpDate);
|
| if (fDate.getTime() <= now) {
|
| if (p.phoneNumbers && p.phoneNumbers.length > 0 && p.followUpMessage) {
|
|
|
| let customizedMsg = p.followUpMessage;
|
| if (p.clientName) {
|
| customizedMsg = customizedMsg.replace(/{client_name}/g, p.clientName);
|
| } else {
|
| customizedMsg = customizedMsg.replace(/{client_name}/g, 'Valued Customer');
|
| }
|
|
|
| const targetRecipients = p.phoneNumbers.map(num => ({ phone: num, name: p.clientName || 'Recipient' }));
|
|
|
| console.log(`[Scheduler] Dispatching automated WhatsApp follow-up for client template block: ${p.clientName || 'Recipient'}`);
|
|
|
| chrome.runtime.sendMessage({
|
| action: 'start_wa_campaign',
|
| recipients: targetRecipients,
|
| text: customizedMsg,
|
| media: null,
|
| delayConfig: { minSeconds: 15, maxSeconds: 45 }
|
| });
|
|
|
| p.followUpSent = true;
|
| changed = true;
|
| }
|
| }
|
| }
|
| }
|
|
|
| if (changed) {
|
| await savePrevPostsToLocal();
|
| const inlinePanel = document.getElementById('previous-posts-inline-panel');
|
| if (inlinePanel) {
|
| renderPreviousPostsGrid();
|
| renderEvaluationDashboard();
|
| }
|
| }
|
| }
|
|
|
| async function openVariantTraceModal(postId) {
|
| const tbody = document.getElementById('variant-trace-tbody');
|
| const metaInfo = document.getElementById('variant-trace-meta-info');
|
| if (!tbody || !metaInfo) return;
|
|
|
| tbody.innerHTML = '';
|
| const post = previousPostsData.find(p => p.id === postId);
|
| if (!post) return;
|
|
|
| metaInfo.textContent = `Delivery Trace Map for: "${post.title}" | Cumulative Performance Rating: ${(post.score || 0).toLocaleString('en-US')}%`;
|
|
|
| const campaignHistoryData = await FritreeStorage.get('campaignHistoryData', []);
|
| let matchCount = 0;
|
|
|
| campaignHistoryData.forEach(camp => {
|
| if (camp.logs && Array.isArray(camp.logs)) {
|
| camp.logs.forEach(log => {
|
| if (log.variantName === postId) {
|
| matchCount++;
|
| const tr = document.createElement('tr');
|
|
|
| const tdCamp = document.createElement('td');
|
| tdCamp.style.fontWeight = "bold";
|
| tdCamp.textContent = camp.name;
|
|
|
| const tdTarget = document.createElement('td');
|
| tdTarget.textContent = log.groupId || "Unknown";
|
|
|
| const tdStatus = document.createElement('td');
|
| const badge = document.createElement('span');
|
| badge.className = `status-badge ${log.status === 'success' ? 'success' : (log.status === 'pending' ? 'pending' : 'failed')}`;
|
| badge.textContent = log.status === 'success' ? 'Dispatched' : (log.status === 'pending' ? 'Moderation' : 'Failed');
|
| tdStatus.appendChild(badge);
|
|
|
| const tdLink = document.createElement('td');
|
| if (log.postUrl) {
|
| const link = document.createElement('a');
|
| link.href = log.postUrl;
|
| link.target = '_blank';
|
| link.style.cssText = 'color:#1877f2; font-weight:bold; text-decoration:none;';
|
| link.innerHTML = '<i class="fa-solid fa-up-right-from-square"></i>View';
|
| tdLink.appendChild(link);
|
| } else {
|
| tdLink.textContent = log.error || 'N/A';
|
| }
|
|
|
| tr.appendChild(tdCamp);
|
| tr.appendChild(tdTarget);
|
| tr.appendChild(tdStatus);
|
| tr.appendChild(tdLink);
|
| tbody.appendChild(tr);
|
| }
|
| });
|
| }
|
| });
|
|
|
| if (matchCount === 0) {
|
| tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color:var(--text-muted); padding:15px;"><i class="fa-solid fa-folder-open"></i>No active dispatches associated with this template block found in the ledger.</td></tr>';
|
| }
|
|
|
| const modal = document.getElementById('variant-trace-modal');
|
| if (modal) modal.style.display = 'flex';
|
| }
|
|
|
| |
| |
|
|
| function triggerOnScreenCelebration() {
|
| console.log("[Fritree Celebration] Action completed instantly.");
|
| }
|
|
|
|
|
| global.FritreeRotation = {
|
| init: initRotationModule,
|
| isRotationActive: () => isRotationActive,
|
| getSelectedIds: () => Array.from(selectedPrevPostIds),
|
| getPosts: () => previousPostsData,
|
| disableRotation: () => {
|
| isRotationActive = false;
|
| const indicatorPanel = document.getElementById('prev-posts-indicator-panel');
|
| const composerPanel = document.getElementById('default-composer-panel');
|
| const postTextarea = document.getElementById('post-text');
|
|
|
| if (indicatorPanel) indicatorPanel.style.display = 'none';
|
| if (composerPanel) composerPanel.style.opacity = '1';
|
| if (postTextarea) postTextarea.disabled = false;
|
| },
|
| incrementUsage: incrementPrevPostUsage,
|
| saveDraft: autoSaveCurrentPostToLibrary,
|
| recalculateScores: recalculateAllPostScores,
|
| selectNextPost: selectNextRotationPost,
|
| triggerFollowUps: checkAndTriggerFollowUps,
|
| celebrate: triggerOnScreenCelebration,
|
| showTrace: openVariantTraceModal
|
| };
|
|
|
| if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
| initRotationModule();
|
| } else {
|
| document.addEventListener('DOMContentLoaded', () => initRotationModule());
|
| }
|
|
|
| })(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this); |