(function () { 'use strict'; const UNIT_PRICE = 500000; const REUSE = { new_block: 1, no_change: 0.10, minor: 0.25, medium: 0.40, major: 0.70 }; const REUSE_ONLY = { no_change: 0.10, minor: 0.25, medium: 0.40, major: 0.70 }; const CATALOG = { general: { label: 'عمومی', hasComplexity: true, blocks: { Navbar: { simple: 1, medium: 1.5, complex: 2 }, Sidebar: { simple: 1, medium: 2, complex: 3 }, Footer: { simple: 0.8, medium: 1, complex: 1.5 }, Hero: { simple: 2, medium: 3, complex: 4 }, Banner: { simple: 1, medium: 1.5, complex: 2 }, CTA: { simple: 0.8, medium: 1, complex: 1.5 }, 'Feature Grid': { simple: 1, medium: 2, complex: 3 }, 'Card Section': { simple: 1, medium: 2, complex: 3 }, FAQ: { simple: 1, medium: 1.5, complex: 2 }, Timeline: { simple: 1, medium: 2, complex: 3 }, Testimonial: { simple: 1, medium: 2, complex: 3 }, Pricing: { simple: 2, medium: 3, complex: 4 }, Gallery: { simple: 2, medium: 3, complex: 5 }, 'Blog List': { simple: 2, medium: 3, complex: 4 }, 'Contact Section': { simple: 1, medium: 2, complex: 3 } } }, dashboard: { label: 'داشبورد', hasComplexity: true, blocks: { 'Stat Card': { simple: 0.5, medium: 0.8, complex: 1 }, Widget: { simple: 1, medium: 2, complex: 3 }, 'Data Table': { simple: 2, medium: 4, complex: 6 }, 'Filter Bar': { simple: 1, medium: 2, complex: 3 }, Search: { simple: 0.5, medium: 1, complex: 1.5 }, Pagination: { simple: 0.5, medium: 0.8, complex: 1 }, Tabs: { simple: 0.8, medium: 1, complex: 2 }, Chart: { simple: 2, medium: 3, complex: 5 }, Calendar: { simple: 2, medium: 4, complex: 6 }, Kanban: { simple: 4, medium: 6, complex: 8 } } }, form: { label: 'فرم', hasComplexity: true, blocks: { 'Form Section': { simple: 1, medium: 2, complex: 3 }, 'Input Group': { simple: 0.3, medium: 0.5, complex: 1 }, 'File Upload': { simple: 1, medium: 2, complex: 3 }, 'Rich Text': { simple: 2, medium: 3, complex: 4 }, 'Multi Step': { simple: 3, medium: 5, complex: 8 }, 'Dynamic Field': { simple: 2, medium: 4, complex: 6 } } }, interactive: { label: 'تعاملی', hasComplexity: false, blocks: { Modal: 1, Drawer: 1, Dropdown: 0.5, Popover: 0.5, 'Command Menu': 1, Toast: 0.5, Skeleton: 0.5, 'Loading State': 0.5, 'Empty State': 0.5, 'Error State': 0.5 } } }; const COMPLEXITY_LABELS = { simple: 'Simple', medium: 'Medium', complex: 'Complex' }; const REUSE_LABELS = { new_block: 'جدید (۱۰۰٪)', no_change: 'Reuse بدون تغییر (۱۰٪)', minor: 'تغییر جزئی (۲۵٪)', medium: 'تغییر متوسط (۴۰٪)', major: 'تغییر زیاد (۷۰٪)' }; const REUSE_ONLY_LABELS = { no_change: 'بدون تغییر (۱۰٪)', minor: 'تغییر جزئی (۲۵٪)', medium: 'تغییر متوسط (۴۰٪)', major: 'تغییر زیاد (۷۰٪)' }; const CAT_LABELS = { general: 'عمومی', dashboard: 'داشبورد', form: 'فرم', interactive: 'تعاملی' }; const DEFAULT_ROWS = []; const LAYOUT_BLOCKS = { Navbar: true, Footer: true }; const BLOCK_SIMILARITY_GROUPS = [ ['Hero', 'Banner', 'CTA'], ['Feature Grid', 'Card Section', 'Gallery', 'Pricing'], ['Testimonial', 'FAQ', 'Timeline'], ['Blog List', 'Card Section', 'Feature Grid', 'Gallery'], ['Contact Section', 'Form Section', 'CTA', 'Banner'], ['Navbar', 'Sidebar'], ['Stat Card', 'Widget', 'Chart'], ['Data Table', 'Filter Bar', 'Search', 'Pagination', 'Tabs'], ['Modal', 'Drawer', 'Popover', 'Dropdown', 'Toast', 'Command Menu'], ['Form Section', 'Input Group', 'File Upload', 'Multi Step', 'Dynamic Field'] ]; const REUSE_MATCH_THRESHOLD = 18; /** @type {Record} */ var urlAnalysisCache = Object.create(null); let pageIdCounter = 0; let rowIdCounter = 0; let rowsEl, emptyEl, tabsEl, pagesListEl, archiveListEl, calcMainEl; let activePageId = null; let isRendering = false; const state = { pages: [], reportEdited: false }; function escapeHtml(str) { return String(str == null ? '' : str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function getReportBodyEl() { return document.getElementById('calc-project-report-body'); } function getProjectTitle() { var el = document.getElementById('opt-project-title'); return el ? el.value.trim() : ''; } function buildReportHeading() { var title = getProjectTitle(); if (title) return 'گزارش برآورد پروژه — ' + title; return 'گزارش برآورد پروژه'; } function sanitizeFilename(name) { return String(name) .replace(/[^\w\u0600-\u06FF\-]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 60) || 'project'; } function getReportDownloadBasename() { var title = getProjectTitle(); var stamp = new Date().toISOString().slice(0, 10); if (title) return 'project-report-' + sanitizeFilename(title) + '-' + stamp; return 'project-report-' + stamp; } function saveProjectTitleToStorage() { try { localStorage.setItem('feep-price-project-title', getProjectTitle()); } catch (e) { /* ignore quota */ } } function loadProjectTitleFromStorage() { var el = document.getElementById('opt-project-title'); if (!el) return; try { var saved = localStorage.getItem('feep-price-project-title'); if (saved != null) el.value = saved; } catch (e) { /* ignore */ } } function nextPageId() { pageIdCounter += 1; return pageIdCounter; } function nextRowId() { rowIdCounter += 1; return rowIdCounter; } function fmt(n) { return Math.round(n).toLocaleString('fa-IR'); } function fmtUnit(n) { const s = n % 1 === 0 ? n.toFixed(0) : n.toFixed(1); return parseFloat(s).toLocaleString('fa-IR', { maximumFractionDigits: 1 }); } function getBlockUnit(catKey, blockName, complexity) { const cat = CATALOG[catKey]; if (!cat) return 0; const block = cat.blocks[blockName]; if (block == null) return 0; if (!cat.hasComplexity) return block; return block[complexity] ?? 0; } function buildOptions(obj, labels) { return Object.keys(obj).map(function (k) { return ''; }).join(''); } function getActivePage() { return state.pages.find(function (p) { return p.id === activePageId; }); } function getPageIndex(pageId) { return state.pages.findIndex(function (p) { return p.id === pageId; }); } function createPageData(name, rows) { return { id: nextPageId(), name: name, archived: false, rows: (rows || []).map(function (r) { return Object.assign({ id: nextRowId() }, r); }), animation: '0', animUnits: 2, pageMultiplier: 1 }; } function parsePageMultiplier(val) { var n = parseInt(val, 10); if (!n || n < 1) return 1; return Math.min(n, 999); } function getPageMultiplier(page) { if (!page) return 1; return parsePageMultiplier(page.pageMultiplier); } function getDraftPages() { return state.pages.filter(function (p) { return !p.archived; }); } function getArchivedPages() { return state.pages.filter(function (p) { return p.archived; }); } function isPageReadOnly(page) { return page && page.archived; } function getNextDraftPageName() { var n = state.pages.length + 1; return n === 1 ? 'Home' : 'صفحه ' + n; } function rowDataFromDom(rowEl) { var data = { id: parseInt(rowEl.dataset.id, 10), mode: rowEl.dataset.mode || 'new', cat: rowEl.querySelector('.sel-cat').value, block: rowEl.querySelector('.sel-block').value, complexity: rowEl.querySelector('.sel-complexity').value, reuse: rowEl.querySelector('.sel-reuse').value }; if (data.mode === 'reuse') { data.sourcePageId = parseInt(rowEl.dataset.sourcePageId, 10); data.sourceRowId = parseInt(rowEl.dataset.sourceRowId, 10); if (rowEl.dataset.reuseMatchType) data.reuseMatchType = rowEl.dataset.reuseMatchType; } return data; } function saveActivePageFromDom() { var page = getActivePage(); if (!page || !rowsEl) return; if (!page.archived) { page.name = document.getElementById('calc-page-name').value.trim() || page.name; page.animation = document.getElementById('opt-animation').value; page.animUnits = parseFloat(document.getElementById('opt-anim-units').value) || 2; } var multEl = document.getElementById('opt-page-multiplier'); if (multEl) page.pageMultiplier = parsePageMultiplier(multEl.value); page.rows = Array.from(rowsEl.querySelectorAll('.calc-row')).map(rowDataFromDom); sanitizePageLayoutRows(page); } function getAnimUnitsForPage(page) { if (page.animation === 'complex') { return Math.min(3, Math.max(1, page.animUnits || 2)); } return parseFloat(page.animation) || 0; } function calcRowUnits(rowData) { var base = getBlockUnit(rowData.cat, rowData.block, rowData.complexity); var reuseKey = rowData.mode === 'reuse' ? rowData.reuse : rowData.reuse; var factor = REUSE[reuseKey] != null ? REUSE[reuseKey] : 1; return base * factor; } function calcPageTotals(page) { var blockUnits = 0; getRowsForPage(page).forEach(function (row) { blockUnits += calcRowUnits(row); }); var animUnits = getAnimUnitsForPage(page); var baseTotalUnits = blockUnits + animUnits; var mult = getPageMultiplier(page); var totalUnits = baseTotalUnits * mult; return { blockUnits: blockUnits * mult, animUnits: animUnits * mult, totalUnits: totalUnits, cost: totalUnits * UNIT_PRICE, pageMultiplier: mult, baseTotalUnits: baseTotalUnits }; } function getBlockRegistry() { var registry = []; var seen = Object.create(null); getArchivedPages().forEach(function (page) { page.rows.forEach(function (row) { if (row.mode === 'reuse') return; if (row.reuse !== 'new_block') return; var key = row.cat + '::' + row.block; if (seen[key]) return; seen[key] = true; registry.push({ sourcePageId: page.id, sourceRowId: row.id, pageName: page.name, cat: row.cat, block: row.block, complexity: row.complexity, label: row.label || row.block, fingerprint: row.fingerprint || '', key: key }); }); }); return registry; } function getSequentialPageNumber() { return getArchivedPages().length + 1; } function normalizeImportUrl(url) { try { var parsed = new URL(url); var host = parsed.hostname.replace(/^www\./i, '').toLowerCase(); var path = parsed.pathname.replace(/\/+$/, '') || '/'; return parsed.protocol + '//' + host + path + (parsed.search || ''); } catch (e) { return (url || '').trim().toLowerCase(); } } function findPageBySourceUrl(url) { var key = normalizeImportUrl(url); return state.pages.find(function (p) { return p.sourceUrl && normalizeImportUrl(p.sourceUrl) === key; }) || null; } function getCachedUrlAnalysis(url) { return urlAnalysisCache[normalizeImportUrl(url)] || null; } function cacheUrlAnalysis(url, parsed) { urlAnalysisCache[normalizeImportUrl(url)] = { rows: parsed.rows.map(function (r) { return Object.assign({}, r); }), pageTitle: parsed.pageTitle, meta: Object.assign({}, parsed.meta || {}), analyzed: parsed.analyzed ? parsed.analyzed.map(function (a) { return Object.assign({}, a); }) : undefined }; } function resolveUrlAnalysis(url, html) { var cached = getCachedUrlAnalysis(url); if (cached) { return { rows: cached.rows.map(function (r) { return Object.assign({}, r); }), pageTitle: cached.pageTitle, meta: Object.assign({}, cached.meta), analyzed: cached.analyzed ? cached.analyzed.map(function (a) { return Object.assign({}, a); }) : undefined }; } var parsed = parseBlocksFromHtml(html); cacheUrlAnalysis(url, parsed); return parsed; } function isLayoutBlock(blockName) { return !!LAYOUT_BLOCKS[blockName]; } function getProjectPageNumber(page) { if (!page) return 1; if (page.projectPageNumber) return page.projectPageNumber; var archived = getArchivedPages(); var idx = archived.findIndex(function (p) { return p.id === page.id; }); if (idx >= 0) return idx + 1; return archived.length + 1; } function shouldExcludeLayoutForPage(page) { return getProjectPageNumber(page) > 1; } function filterLayoutRows(rows) { return rows.filter(function (row) { return !isLayoutBlock(row.block); }); } function getRowsForPage(page) { if (!page || !page.rows) return []; if (!shouldExcludeLayoutForPage(page)) return page.rows; return filterLayoutRows(page.rows); } function sanitizePageLayoutRows(page) { if (!page || !page.rows) return; if (!shouldExcludeLayoutForPage(page)) return; page.rows = filterLayoutRows(page.rows); } function getReuseRegistryForPage(page) { var registry = getBlockRegistry(); if (!shouldExcludeLayoutForPage(page)) return registry; return registry.filter(function (r) { return !isLayoutBlock(r.block); }); } function getBlockSimilarityGroup(blockName) { for (var i = 0; i < BLOCK_SIMILARITY_GROUPS.length; i++) { if (BLOCK_SIMILARITY_GROUPS[i].indexOf(blockName) !== -1) { return BLOCK_SIMILARITY_GROUPS[i]; } } return null; } function blocksAreSimilar(blockA, blockB) { if (!blockA || !blockB) return false; if (blockA === blockB) return true; var group = getBlockSimilarityGroup(blockA); return group ? group.indexOf(blockB) !== -1 : false; } function tokenizeForSimilarity(str) { return (str || '').toLowerCase() .replace(/[^\u0600-\u06FFa-z0-9\s]/g, ' ') .split(/\s+/) .filter(function (t) { return t.length > 2; }); } function textSimilarity(a, b) { var tokensA = tokenizeForSimilarity(a); var tokensB = tokenizeForSimilarity(b); if (!tokensA.length || !tokensB.length) return 0; var overlap = 0; tokensA.forEach(function (t) { if (tokensB.indexOf(t) !== -1) overlap += 1; }); return overlap / Math.max(tokensA.length, tokensB.length); } function fingerprintSimilarity(a, b) { if (!a || !b) return 0; if (a === b) return 1; var partsA = a.split('|').filter(Boolean); var partsB = b.split('|').filter(Boolean); if (!partsA.length || !partsB.length) return textSimilarity(a, b); var overlap = 0; partsA.forEach(function (p) { if (partsB.indexOf(p) !== -1) overlap += 1; }); return overlap / Math.max(partsA.length, partsB.length); } function scoreReuseMatch(candidate, cat, block, meta) { var score = 0; var matchType = 'similar'; if (candidate.key === cat + '::' + block) { score += 100; matchType = 'exact'; } if (blocksAreSimilar(block, candidate.block)) { score += 45; if (matchType !== 'exact') matchType = 'similar-type'; } if (candidate.cat === cat) { score += 22; if (score < 45) matchType = 'same-category'; } if (meta && meta.fingerprint && candidate.fingerprint) { var fpScore = fingerprintSimilarity(meta.fingerprint, candidate.fingerprint); if (fpScore >= 0.4) { score += Math.round(fpScore * 35); if (matchType === 'similar') matchType = 'fingerprint'; } } if (meta && meta.label && candidate.label) { var labelScore = textSimilarity(meta.label, candidate.label); if (labelScore >= 0.35) { score += Math.round(labelScore * 25); } } return { score: score, matchType: matchType }; } function buildRegistryEntry(sourcePageId, sourceRowId, pageName, row) { return { sourcePageId: sourcePageId, sourceRowId: sourceRowId, pageName: pageName, cat: row.cat, block: row.block, complexity: row.complexity, label: row.label || row.block, fingerprint: row.fingerprint || '', key: row.cat + '::' + row.block }; } function findReuseInRegistry(registry, cat, block, meta) { if (!registry.length) return null; var best = null; var bestScore = REUSE_MATCH_THRESHOLD; var bestType = 'similar'; registry.forEach(function (candidate) { var scored = scoreReuseMatch(candidate, cat, block, meta || {}); if (scored.score > bestScore) { bestScore = scored.score; best = candidate; bestType = scored.matchType; } else if (scored.score === bestScore && best) { if (candidate.sourcePageId < best.sourcePageId || (candidate.sourcePageId === best.sourcePageId && candidate.sourceRowId < best.sourceRowId)) { best = candidate; bestType = scored.matchType; } } }); if (!best) return null; return { source: best, matchType: bestType, score: bestScore }; } function findIntraPageReuse(localRegistry, cat, block, meta) { if (!localRegistry.length) return null; for (var i = 0; i < localRegistry.length; i++) { var candidate = localRegistry[i]; if (candidate.cat === cat && candidate.block === block) { return { source: candidate, matchType: 'exact', score: 100 }; } } if (meta && meta.fingerprint) { for (var j = 0; j < localRegistry.length; j++) { var fpCandidate = localRegistry[j]; if (fpCandidate.fingerprint && fpCandidate.fingerprint === meta.fingerprint) { return { source: fpCandidate, matchType: 'exact', score: 100 }; } } } return findReuseInRegistry(localRegistry, cat, block, meta); } function ensureLocalRegistryEntry(localRegistry, resultIndex, row) { var key = row.cat + '::' + row.block; var hasKey = localRegistry.some(function (c) { return c.key === key; }); var hasFingerprint = row.fingerprint && localRegistry.some(function (c) { return c.fingerprint && c.fingerprint === row.fingerprint; }); if (hasKey || hasFingerprint) return; localRegistry.push(buildRegistryEntry( INTRA_PAGE_SOURCE, resultIndex, 'همین صفحه', row )); } function findReuseSource(cat, block, meta) { return findReuseInRegistry(getBlockRegistry(), cat, block, meta); } var INTRA_PAGE_SOURCE = '__same_page__'; function resolveIntraPageReuseReferences(page) { if (!page || !page.rows) return; var indexToId = page.rows.map(function (r) { return r.id; }); page.rows.forEach(function (row) { if (row.sourcePageId !== INTRA_PAGE_SOURCE) return; var idx = row.sourceRowId; if (typeof idx === 'number' && indexToId[idx] != null) { row.sourcePageId = page.id; row.sourceRowId = indexToId[idx]; row.sourcePageName = page.name; } }); } function dedupeRowsWithinPage(page) { if (!page || !page.rows || page.rows.length < 2) return 0; var localRegistry = []; var reuseCount = 0; page.rows.forEach(function (row) { var meta = { fingerprint: row.fingerprint || '', label: row.label || row.block }; if (row.mode === 'reuse') { var key = row.cat + '::' + row.block; var hasKey = localRegistry.some(function (c) { return c.key === key; }); var hasFingerprint = row.fingerprint && localRegistry.some(function (c) { return c.fingerprint && c.fingerprint === row.fingerprint; }); if (!hasKey && !hasFingerprint) { localRegistry.push(buildRegistryEntry(page.id, row.id, page.name, row)); } return; } if (row.reuse !== 'new_block') return; var match = findIntraPageReuse(localRegistry, row.cat, row.block, meta); if (match) { row.mode = 'reuse'; row.sourcePageId = page.id; row.sourceRowId = match.source.sourceRowId; row.sourcePageName = page.name; row.reuse = reuseLevelForMatch(match.matchType); row.reuseMatchType = match.matchType; reuseCount += 1; } else { localRegistry.push(buildRegistryEntry(page.id, row.id, page.name, row)); } }); return reuseCount; } function reuseLevelForMatch(matchType) { if (matchType === 'exact') return 'no_change'; if (matchType === 'similar-type' || matchType === 'fingerprint') return 'minor'; return 'medium'; } function matchRowsAsNew(rawRows, options) { return matchRowsWithReuse(rawRows, Object.assign({}, options || {}, { allowArchiveReuse: false })); } function matchRowsWithReuse(rawRows, options) { var excludeLayout = options && options.excludeLayout; var excludeSourcePageId = options && options.excludeSourcePageId; var allowArchiveReuse = !options || options.allowArchiveReuse !== false; var result = []; var localRegistry = []; var stats = { newCount: 0, reuseCount: 0, skippedLayout: 0 }; rawRows.forEach(function (row) { if (excludeLayout && isLayoutBlock(row.block)) { stats.skippedLayout += 1; return; } var meta = { fingerprint: row.fingerprint, label: row.label }; var match = findIntraPageReuse(localRegistry, row.cat, row.block, meta); if (!match && allowArchiveReuse) { match = findReuseSource(row.cat, row.block, meta); if (match && excludeSourcePageId && match.source.sourcePageId === excludeSourcePageId) { match = null; } } if (match) { result.push({ mode: 'reuse', sourcePageId: match.source.sourcePageId, sourceRowId: match.source.sourceRowId, sourcePageName: match.source.pageName, cat: row.cat, block: row.block, complexity: row.complexity || match.source.complexity, reuse: reuseLevelForMatch(match.matchType), fingerprint: row.fingerprint || '', label: row.label || row.block, reuseMatchType: match.matchType }); stats.reuseCount += 1; ensureLocalRegistryEntry(localRegistry, result.length - 1, row); } else { result.push({ mode: 'new', cat: row.cat, block: row.block, complexity: row.complexity, reuse: 'new_block', fingerprint: row.fingerprint || '', label: row.label || row.block }); ensureLocalRegistryEntry(localRegistry, result.length - 1, row); stats.newCount += 1; } }); return { rows: result, stats: stats }; } function getContentPercent() { var v = document.getElementById('opt-content').value; if (v === '0.05_img') return 0.05; return parseFloat(v) || 0; } function getRevisionUnits() { var el = document.getElementById('opt-revision-units'); if (!el) return 0; var n = parseFloat(el.value); return Math.max(0, n || 0); } function getSelectLabel(id) { var el = document.getElementById(id); if (!el || !el.options.length) return '—'; return el.options[el.selectedIndex].textContent.trim(); } function getAnimationLabel(page) { if (!page) return '—'; if (page.animation === 'complex') { return 'Motion پیچیده — ' + fmtUnit(getAnimUnitsForPage(page)) + ' Unit'; } if (page.animation === '0') return 'Hover Effect — رایگان'; if (page.animation === '0.2') return 'Transition — ۰.۲ Unit'; if (page.animation === '0.5') return 'Motion ساده — ۰.۵ Unit'; return '—'; } function getReuseStatusLabel(row) { if (row.mode === 'reuse') { return REUSE_ONLY_LABELS[row.reuse] || row.reuse; } return REUSE_LABELS[row.reuse] || REUSE_LABELS.new_block; } function getProjectFinancials() { var projectBlockUnits = 0; var projectAnimUnits = 0; state.pages.forEach(function (page) { var totals = calcPageTotals(page); projectBlockUnits += totals.blockUnits; projectAnimUnits += totals.animUnits; }); var revisionUnits = getRevisionUnits(); var totalUnits = projectBlockUnits + projectAnimUnits + revisionUnits; var blocksCost = totalUnits * UNIT_PRICE; var contentPct = getContentPercent(); var responsivePct = parseFloat(document.getElementById('opt-responsive').value) || 0; var darkPct = parseFloat(document.getElementById('opt-dark').value) || 0; var bilingualPct = parseFloat(document.getElementById('opt-bilingual').value) || 0; var revisionCost = revisionUnits * UNIT_PRICE; var contentExtra = blocksCost * contentPct; var afterContent = blocksCost + contentExtra; var responsiveExtra = afterContent * responsivePct; var afterResponsive = afterContent + responsiveExtra; var darkExtra = afterResponsive * darkPct; var afterDark = afterResponsive + darkExtra; var bilingualExtra = afterDark * bilingualPct; var total = afterDark + bilingualExtra; return { projectBlockUnits: projectBlockUnits, projectAnimUnits: projectAnimUnits, revisionUnits: revisionUnits, totalUnits: totalUnits, blocksCost: blocksCost, contentExtra: contentExtra, responsiveExtra: responsiveExtra, darkExtra: darkExtra, bilingualExtra: bilingualExtra, revisionCost: revisionCost, total: total }; } function formatReportDate(date) { try { return date.toLocaleString('fa-IR', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch (e) { return date.toISOString(); } } function getRowReportDetail(row) { var complexity = CATALOG[row.cat] && CATALOG[row.cat].hasComplexity ? COMPLEXITY_LABELS[row.complexity] || row.complexity : '—'; var status = row.mode === 'reuse' ? 'Reuse' : 'جدید'; return complexity + ' · ' + status; } function reportKvTable(rows) { var html = ''; rows.forEach(function (row) { html += ''; }); html += '
' + escapeHtml(row[0]) + '' + escapeHtml(row[1]) + '
'; return html; } function reportBlocksTableColgroup() { return '' + '' + '' + '' + '' + ''; } function buildProjectReportHtml() { var archived = getArchivedPages(); var drafts = getDraftPages(); var finances = getProjectFinancials(); var now = new Date(); var parts = []; parts.push('
'); parts.push('

' + escapeHtml(buildReportHeading()) + '

'); var metaRows = [ ['عنوان پروژه', getProjectTitle() || '—'], ['تاریخ گزارش', formatReportDate(now)], ['تعرفه هر Unit', fmt(UNIT_PRICE) + ' تومان'], ['صفحات تأییدشده', archived.length.toLocaleString('fa-IR')] ]; if (drafts.length) { metaRows.push(['پیش‌نویس‌های باز', drafts.length.toLocaleString('fa-IR') + ' — ' + drafts.map(function (p) { return p.name; }).join('، ')]); } parts.push(reportKvTable(metaRows)); parts.push('

گزینه‌های پروژه

'); parts.push(reportKvTable([ ['محتوا', getSelectLabel('opt-content')], ['Responsive', getSelectLabel('opt-responsive')], ['Dark Mode', getSelectLabel('opt-dark')], ['دوزبانه', getSelectLabel('opt-bilingual')], ['Revision', finances.revisionUnits ? fmtUnit(finances.revisionUnits) + ' Unit' : 'ندارد'] ])); if (!archived.length) { parts.push('

هنوز صفحه‌ای آرشیو نشده — بعد از تحلیل URL، صفحه را آرشیو کنید.

'); } else { parts.push('

بلاک‌های صفحات

'); parts.push('' + reportBlocksTableColgroup()); parts.push(''); ['Block', 'جزئیات', 'Unit', 'هزینه (تومان)'].forEach(function (h) { parts.push(''); }); parts.push(''); archived.forEach(function (page, pageIdx) { var totals = calcPageTotals(page); var pageLabel = 'صفحه ' + (pageIdx + 1).toLocaleString('fa-IR') + ': ' + page.name; if (totals.pageMultiplier > 1) { pageLabel += ' (ضریب ×' + totals.pageMultiplier.toLocaleString('fa-IR') + ' — ' + totals.pageMultiplier.toLocaleString('fa-IR') + ' صفحه مشابه)'; } if (page.sourceUrl) pageLabel += ' — ' + page.sourceUrl; parts.push(''); var rows = getRowsForPage(page); if (!rows.length) { parts.push(''); } else { rows.forEach(function (row) { var units = calcRowUnits(row); var cost = units * UNIT_PRICE; var blockLabel = row.block; if (row.label && row.label !== row.block) blockLabel += ' — ' + row.label; parts.push(''); parts.push(''); parts.push(''); parts.push(''); parts.push(''); parts.push(''); }); } parts.push(''); var subtotalLabel = 'جمع «' + page.name + '» — ' + getAnimationLabel(page); if (totals.pageMultiplier > 1) { subtotalLabel += ' — ضریب ×' + totals.pageMultiplier.toLocaleString('fa-IR') + ' (' + fmtUnit(totals.baseTotalUnits) + ' × ' + totals.pageMultiplier.toLocaleString('fa-IR') + ')'; } parts.push(''); parts.push(''); parts.push(''); parts.push(''); }); parts.push('
' + escapeHtml(h) + '
' + escapeHtml(pageLabel) + '
(بدون بلاک)
' + escapeHtml(blockLabel) + '' + escapeHtml(getRowReportDetail(row)) + '' + escapeHtml(fmtUnit(units)) + '' + escapeHtml(fmt(cost)) + '
' + escapeHtml(subtotalLabel) + '' + escapeHtml(fmtUnit(totals.totalUnits)) + '' + escapeHtml(fmt(totals.cost)) + '
'); } parts.push('

خلاصه مالی

'); var summaryRows = [ ['Unit بلاک‌ها', fmtUnit(finances.projectBlockUnits)], ['Unit انیمیشن', fmtUnit(finances.projectAnimUnits)] ]; if (finances.revisionUnits) summaryRows.push(['Revision', fmtUnit(finances.revisionUnits) + ' Unit']); summaryRows.push(['مجموع Unit', fmtUnit(finances.totalUnits)]); summaryRows.push(['هزینه Blockها', fmt(finances.blocksCost) + ' تومان']); if (finances.contentExtra) summaryRows.push(['اضافه محتوا', fmt(finances.contentExtra) + ' تومان']); if (finances.responsiveExtra) summaryRows.push(['اضافه Responsive', fmt(finances.responsiveExtra) + ' تومان']); if (finances.darkExtra) summaryRows.push(['اضافه Dark Mode', fmt(finances.darkExtra) + ' تومان']); if (finances.bilingualExtra) summaryRows.push(['اضافه دوزبانه', fmt(finances.bilingualExtra) + ' تومان']); if (finances.revisionCost) summaryRows.push(['هزینه Revision', fmt(finances.revisionCost) + ' تومان']); summaryRows.push(['قیمت نهایی', fmt(finances.total) + ' تومان']); parts.push(reportKvTable(summaryRows)); parts.push('
'); return parts.join(''); } function buildProjectReportText() { var body = getReportBodyEl(); if (body && body.innerText) return body.innerText.trim(); var tmp = document.createElement('div'); tmp.innerHTML = buildProjectReportHtml(); return tmp.innerText.trim(); } function saveReportToStorage() { var body = getReportBodyEl(); if (!body) return; try { localStorage.setItem('feep-price-report-html', body.innerHTML); } catch (e) { /* ignore quota */ } } function renderProjectReport(pageTotalsMap, force) { var body = getReportBodyEl(); if (!body) return; if (state.reportEdited && !force) return; body.innerHTML = buildProjectReportHtml(); saveReportToStorage(); } function refreshProjectReport() { state.reportEdited = false; try { localStorage.removeItem('feep-price-report-html'); } catch (e) { /* ignore */ } renderProjectReport(null, true); showReportToast('گزارش از داده‌های فعلی به‌روزرسانی شد.'); } function onReportEdited() { state.reportEdited = true; saveReportToStorage(); } function showReportToast(message) { var toast = document.getElementById('calc-report-toast'); if (!toast) return; toast.textContent = message; toast.hidden = false; clearTimeout(showReportToast._timer); showReportToast._timer = setTimeout(function () { toast.hidden = true; }, 2500); } function copyProjectReport() { var body = getReportBodyEl(); if (!body) return; var text = buildProjectReportText(); if (!text) return; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { showReportToast('متن گزارش در کلیپ‌بورد کپی شد.'); }).catch(function () { selectProjectReport(); try { document.execCommand('copy'); showReportToast('متن گزارش در کلیپ‌بورد کپی شد.'); } catch (e) { showReportToast('متن را دستی انتخاب و کپی کنید (Ctrl+C).'); } }); return; } selectProjectReport(); document.execCommand('copy'); showReportToast('متن گزارش در کلیپ‌بورد کپی شد.'); } function selectProjectReport() { var body = getReportBodyEl(); if (!body) return; body.focus(); var range = document.createRange(); range.selectNodeContents(body); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } function downloadProjectReport() { var body = getReportBodyEl(); if (!body || !body.innerHTML.trim()) return; var docTitle = getProjectTitle() ? 'گزارش پروژه — ' + getProjectTitle() : 'گزارش برآورد پروژه'; var htmlDoc = '' + '' + escapeHtml(docTitle) + '' + '' + body.innerHTML + ''; var blob = new Blob([htmlDoc], { type: 'text/html;charset=utf-8' }); var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; link.download = getReportDownloadBasename() + '.html'; link.click(); URL.revokeObjectURL(url); showReportToast('فایل HTML گزارش دانلود شد.'); } function downloadProjectReportText() { var text = buildProjectReportText(); if (!text) return; var blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); var url = URL.createObjectURL(blob); var link = document.createElement('a'); link.href = url; link.download = getReportDownloadBasename() + '.txt'; link.click(); URL.revokeObjectURL(url); showReportToast('فایل متنی گزارش دانلود شد.'); } function getReportPrintCss() { var styleEl = document.getElementById('report-print-css'); return styleEl ? styleEl.textContent : ''; } function buildReportPrintDocument() { var body = getReportBodyEl(); var docTitle = getProjectTitle() ? 'گزارش پروژه — ' + getProjectTitle() : 'گزارش برآورد پروژه'; return '' + '' + escapeHtml(docTitle) + '' + '' + '' + '' + '' + '
' + body.innerHTML + '
' + ''; } function downloadProjectReportPdf() { var source = getReportBodyEl(); if (!source || !source.innerHTML.trim()) { if (!getArchivedPages().length) { showReportToast('گزارش خالی است — ابتدا صفحه‌ای آرشیو کنید.'); return; } showReportToast('گزارش خالی است — ابتدا «به‌روزرسانی» را بزنید.'); return; } var btn = document.getElementById('calc-report-download-pdf'); var iframe = document.createElement('iframe'); iframe.setAttribute('aria-hidden', 'true'); iframe.style.cssText = 'position:fixed;width:0;height:0;border:0;opacity:0;pointer-events:none;'; document.body.appendChild(iframe); var cleanedUp = false; var printWin = iframe.contentWindow; var printDoc = printWin.document; function cleanup() { if (cleanedUp) return; cleanedUp = true; printWin.removeEventListener('afterprint', cleanup); if (iframe.parentNode) iframe.parentNode.removeChild(iframe); if (btn) btn.disabled = false; } printWin.addEventListener('afterprint', cleanup); if (btn) btn.disabled = true; showReportToast('پنجره چاپ باز شد — «ذخیره به PDF» یا Microsoft Print to PDF را انتخاب کنید.'); printDoc.open(); printDoc.write(buildReportPrintDocument()); printDoc.close(); function triggerPrint() { printWin.focus(); printWin.print(); window.setTimeout(cleanup, 3000); } if (printDoc.fonts && printDoc.fonts.ready) { printDoc.fonts.ready.then(triggerPrint).catch(triggerPrint); } else { window.setTimeout(triggerPrint, 400); } } function recalculate() { if (!rowsEl) return; if (!isRendering) { saveActivePageFromDom(); } var projectBlockUnits = 0; var projectAnimUnits = 0; var pageTotalsMap = {}; state.pages.forEach(function (page) { var totals = calcPageTotals(page); pageTotalsMap[page.id] = totals; projectBlockUnits += totals.blockUnits; projectAnimUnits += totals.animUnits; }); var revisionUnits = getRevisionUnits(); var totalUnits = projectBlockUnits + projectAnimUnits + revisionUnits; var blocksCost = totalUnits * UNIT_PRICE; var contentPct = getContentPercent(); var responsivePct = parseFloat(document.getElementById('opt-responsive').value) || 0; var darkPct = parseFloat(document.getElementById('opt-dark').value) || 0; var bilingualPct = parseFloat(document.getElementById('opt-bilingual').value) || 0; var revisionCost = revisionUnits * UNIT_PRICE; var contentExtra = blocksCost * contentPct; var afterContent = blocksCost + contentExtra; var responsiveExtra = afterContent * responsivePct; var afterResponsive = afterContent + responsiveExtra; var darkExtra = afterResponsive * darkPct; var afterDark = afterResponsive + darkExtra; var bilingualExtra = afterDark * bilingualPct; var total = afterDark + bilingualExtra; if (emptyEl) { emptyEl.style.display = rowsEl.children.length ? 'none' : 'block'; } var activeTotals = pageTotalsMap[activePageId] || { totalUnits: 0, cost: 0, pageMultiplier: 1 }; var activePage = getActivePage(); var pageTotalLabel = fmt(activeTotals.cost) + ' تومان'; if (activeTotals.pageMultiplier > 1) { pageTotalLabel = '× ' + activeTotals.pageMultiplier.toLocaleString('fa-IR') + ' — ' + pageTotalLabel; } document.getElementById('sum-page-total').textContent = pageTotalLabel; var pageMultHint = document.getElementById('sum-page-multiplier-hint'); if (pageMultHint) { if (activeTotals.pageMultiplier > 1 && activePage) { pageMultHint.hidden = false; pageMultHint.textContent = 'قالب «' + activePage.name + '» — ' + fmtUnit(activeTotals.baseTotalUnits || 0) + ' Unit × ' + activeTotals.pageMultiplier.toLocaleString('fa-IR') + ' = ' + fmtUnit(activeTotals.totalUnits) + ' Unit'; } else { pageMultHint.hidden = true; pageMultHint.textContent = ''; } } document.getElementById('sum-units').textContent = fmtUnit(totalUnits); document.getElementById('sum-blocks').textContent = fmt(blocksCost) + ' تومان'; document.getElementById('sum-content').textContent = contentExtra ? fmt(contentExtra) + ' تومان' : '—'; document.getElementById('sum-responsive').textContent = responsiveExtra ? fmt(responsiveExtra) + ' تومان' : '—'; document.getElementById('sum-dark').textContent = darkExtra ? fmt(darkExtra) + ' تومان' : '—'; document.getElementById('sum-bilingual').textContent = bilingualExtra ? fmt(bilingualExtra) + ' تومان' : '—'; document.getElementById('sum-revision').textContent = revisionCost ? fmt(revisionCost) + ' تومان' : '—'; document.getElementById('sum-total').textContent = fmt(total) + ' تومان'; renderPageTabs(pageTotalsMap); renderPagesList(pageTotalsMap); renderArchiveList(pageTotalsMap); renderProjectReport(pageTotalsMap); updateReuseButton(); updatePageActions(); } function updatePageActions() { var page = getActivePage(); var archiveBtn = document.getElementById('calc-page-archive'); var unarchiveBtn = document.getElementById('calc-page-unarchive'); var banner = document.getElementById('calc-archive-banner'); var readonly = isPageReadOnly(page); if (calcMainEl) { calcMainEl.classList.remove('calc-main--readonly'); } var pageNameEl = document.getElementById('calc-page-name'); if (pageNameEl) pageNameEl.readOnly = readonly; var animEl = document.getElementById('opt-animation'); var animUnitsEl = document.getElementById('opt-anim-units'); if (animEl) animEl.disabled = readonly; if (animUnitsEl) animUnitsEl.disabled = readonly; var multEl = document.getElementById('opt-page-multiplier'); if (multEl) multEl.disabled = false; if (banner) { banner.hidden = !readonly; } if (archiveBtn) { archiveBtn.hidden = readonly; archiveBtn.disabled = !page || !page.rows.length; } if (unarchiveBtn) { unarchiveBtn.hidden = !readonly; } var deleteArchiveBtn = document.getElementById('calc-page-delete-archive'); if (deleteArchiveBtn) { deleteArchiveBtn.hidden = !readonly; } } function renderPageTabs(pageTotalsMap) { if (!tabsEl) return; var drafts = getDraftPages(); if (!drafts.length) { tabsEl.innerHTML = 'پیش‌نویسی نیست — از آرشیو مشاهده کنید'; return; } tabsEl.innerHTML = drafts.map(function (page) { var totals = pageTotalsMap[page.id] || { cost: 0 }; var active = page.id === activePageId ? ' is-active' : ''; var multBadge = totals.pageMultiplier > 1 ? ' ×' + totals.pageMultiplier.toLocaleString('fa-IR') + '' : ''; return ''; }).join(''); tabsEl.querySelectorAll('.calc-page-tab').forEach(function (btn) { btn.addEventListener('click', function () { switchPage(parseInt(btn.dataset.pageId, 10)); }); }); } function renderPagesList(pageTotalsMap) { if (!pagesListEl) return; var drafts = getDraftPages(); if (!drafts.length) { pagesListEl.innerHTML = '
پیش‌نویسی فعال نیست
'; return; } pagesListEl.innerHTML = drafts.map(function (page) { var totals = pageTotalsMap[page.id] || { totalUnits: 0, cost: 0 }; var active = page.id === activePageId && !page.archived ? ' is-active' : ''; var multNote = totals.pageMultiplier > 1 ? ' ×' + totals.pageMultiplier.toLocaleString('fa-IR') : ''; return '
' + '' + page.name + multNote + ' (' + fmtUnit(totals.totalUnits) + 'u)' + '' + fmt(totals.cost) + '' + '
'; }).join(''); pagesListEl.querySelectorAll('.calc-page-summary-item').forEach(function (item) { item.addEventListener('click', function () { switchPage(parseInt(item.dataset.pageId, 10)); }); }); } function renderArchiveList(pageTotalsMap) { if (!archiveListEl) return; var archived = getArchivedPages(); if (!archived.length) { archiveListEl.innerHTML = '
هنوز صفحه‌ای آرشیو نشده
'; return; } archiveListEl.innerHTML = archived.map(function (page) { var totals = pageTotalsMap[page.id] || { totalUnits: 0, cost: 0 }; var viewing = page.id === activePageId && page.archived ? ' is-viewing' : ''; var archMult = totals.pageMultiplier > 1 ? ' ×' + totals.pageMultiplier.toLocaleString('fa-IR') + '' : ''; return '
' + '' + page.name + archMult + 'آرشیو (' + fmtUnit(totals.totalUnits) + 'u)' + '' + '' + fmt(totals.cost) + '' + '' + '' + '
'; }).join(''); archiveListEl.querySelectorAll('.calc-archive-item').forEach(function (item) { item.addEventListener('click', function (e) { if (e.target.closest('.calc-archive-delete')) return; switchPage(parseInt(item.dataset.pageId, 10)); }); }); archiveListEl.querySelectorAll('.calc-archive-delete').forEach(function (btn) { btn.addEventListener('click', function (e) { e.stopPropagation(); deleteArchivedPageById(parseInt(btn.dataset.pageId, 10)); }); }); } function updateReuseButton() { var group = document.getElementById('calc-reuse-group'); var select = document.getElementById('calc-reuse-select'); if (!group || !select || !activePageId) return; var page = getActivePage(); if (!page) { group.hidden = true; return; } var registry = getReuseRegistryForPage(page); if (!registry.length) { group.hidden = true; return; } group.hidden = false; select.innerHTML = registry.map(function (r) { return ''; }).join(''); } function switchPage(pageId) { if (pageId === activePageId) return; saveActivePageFromDom(); activePageId = pageId; renderActivePage(); recalculate(); } function renderActivePage() { var page = getActivePage(); if (!page) return; isRendering = true; document.getElementById('calc-page-name').value = page.name; document.getElementById('opt-animation').value = page.animation; document.getElementById('opt-anim-units').value = page.animUnits; document.getElementById('anim-extra-wrap').classList.toggle('hidden', page.animation !== 'complex'); var multEl = document.getElementById('opt-page-multiplier'); if (multEl) multEl.value = getPageMultiplier(page); document.getElementById('calc-page-delete').disabled = state.pages.length <= 1 || page.archived; sanitizePageLayoutRows(page); rowsEl.innerHTML = ''; getRowsForPage(page).forEach(function (rowData) { if (rowData.mode === 'reuse' && !rowData.sourcePageName) { var srcPage = state.pages.find(function (p) { return p.id === rowData.sourcePageId; }); rowData.sourcePageName = srcPage ? srcPage.name : 'صفحه قبل'; } createRow(rowData); }); if (!getRowsForPage(page).length && emptyEl) { emptyEl.style.display = 'block'; } isRendering = false; } function getSelectableBlocks(catKey, page) { var blocks = Object.keys(CATALOG[catKey].blocks); if (page && shouldExcludeLayoutForPage(page)) { blocks = blocks.filter(function (b) { return !isLayoutBlock(b); }); } return blocks; } function createRow(defaults) { var isReuse = defaults && defaults.mode === 'reuse'; var rowDefaults = defaults || { mode: 'new', cat: 'general', reuse: 'new_block', complexity: 'medium' }; if (!rowDefaults.id) rowDefaults.id = nextRowId(); var cat = rowDefaults.cat || 'general'; var block = rowDefaults.block || Object.keys(CATALOG[cat].blocks)[0]; var row = document.createElement('div'); row.className = 'calc-row' + (isReuse ? ' is-reuse' : ''); row.dataset.id = String(rowDefaults.id); row.dataset.mode = isReuse ? 'reuse' : 'new'; if (isReuse) { row.dataset.sourcePageId = String(rowDefaults.sourcePageId); row.dataset.sourceRowId = String(rowDefaults.sourceRowId); if (rowDefaults.reuseMatchType) row.dataset.reuseMatchType = rowDefaults.reuseMatchType; } var reuseOptions = buildOptions(REUSE, REUSE_LABELS); var badgeHtml = isReuse ? '
↩ Reuse از ' + (rowDefaults.sourcePageName || 'صفحه قبل') + (rowDefaults.reuseMatchType && rowDefaults.reuseMatchType !== 'exact' ? ' (' + (rowDefaults.reuseMatchType === 'similar-type' ? 'بلاک مشابه' : rowDefaults.reuseMatchType === 'fingerprint' ? 'ساختار مشابه' : 'دسته مشابه') + ')' : '') + '
' : ''; row.innerHTML = badgeHtml + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '۰' + '
' + ''; var selCat = row.querySelector('.sel-cat'); var selBlock = row.querySelector('.sel-block'); var selComplexity = row.querySelector('.sel-complexity'); var selReuse = row.querySelector('.sel-reuse'); var complexityField = row.querySelector('.calc-field-complexity'); selCat.value = cat; function populateBlocks() { var c = CATALOG[selCat.value]; var activePage = getActivePage(); selBlock.innerHTML = getSelectableBlocks(selCat.value, activePage).map(function (b) { return ''; }).join(''); if (rowDefaults.block && c.blocks[rowDefaults.block] && (!activePage || !shouldExcludeLayoutForPage(activePage) || !isLayoutBlock(rowDefaults.block))) { selBlock.value = rowDefaults.block; } else if (selBlock.options.length) { selBlock.selectedIndex = 0; } toggleComplexity(); } function toggleComplexity() { var has = CATALOG[selCat.value].hasComplexity; complexityField.style.opacity = has ? '1' : '0.4'; selComplexity.disabled = !has; } function updateUnits() { var base = getBlockUnit(selCat.value, selBlock.value, selComplexity.value); var reuseKey = selReuse.value; var factor = REUSE[reuseKey] != null ? REUSE[reuseKey] : 1; var units = base * factor; var cost = units * UNIT_PRICE; row.querySelector('.unit-val').textContent = fmtUnit(units) + ' Unit'; row.querySelector('.unit-cost').textContent = fmt(cost) + ' تومان'; recalculate(); } selCat.addEventListener('change', function () { populateBlocks(); updateUnits(); }); selBlock.addEventListener('change', updateUnits); selComplexity.addEventListener('change', updateUnits); selReuse.addEventListener('change', function () { if (row.dataset.mode === 'reuse' && selReuse.value === 'new_block') { row.dataset.mode = 'new'; row.classList.remove('is-reuse'); delete row.dataset.sourcePageId; delete row.dataset.sourceRowId; delete row.dataset.reuseMatchType; var badge = row.querySelector('.reuse-badge'); if (badge) badge.remove(); } updateUnits(); }); row.querySelector('.calc-remove').addEventListener('click', function () { row.remove(); recalculate(); }); populateBlocks(); if (rowDefaults.complexity) selComplexity.value = rowDefaults.complexity; if (rowDefaults.reuse) selReuse.value = rowDefaults.reuse; updateUnits(); rowsEl.appendChild(row); if (emptyEl) emptyEl.style.display = 'none'; return row; } function addReuseRow() { var page = getActivePage(); if (!page) return; var registry = getReuseRegistryForPage(page); if (!registry.length) return; var select = document.getElementById('calc-reuse-select'); var key = select ? select.value : ''; var choice = registry.find(function (r) { return (r.sourcePageId + ':' + r.sourceRowId) === key; }) || registry[0]; if (!page) return; var newRow = { id: nextRowId(), mode: 'reuse', sourcePageId: choice.sourcePageId, sourceRowId: choice.sourceRowId, sourcePageName: choice.pageName, cat: choice.cat, block: choice.block, complexity: choice.complexity, reuse: 'no_change' }; createRow(newRow); recalculate(); } function addPage() { saveActivePageFromDom(); var page = createPageData(getNextDraftPageName(), []); page.projectPageNumber = getArchivedPages().length + 1; state.pages.push(page); activePageId = page.id; renderActivePage(); recalculate(); } function archivePage() { var page = getActivePage(); if (!page || page.archived) return; saveActivePageFromDom(); if (!page.rows.length) return; if (!page.projectPageNumber) { page.projectPageNumber = getArchivedPages().length + 1; } sanitizePageLayoutRows(page); page.archived = true; if (!page.archivedAt) { page.archivedAt = new Date().toISOString(); } var drafts = getDraftPages(); if (drafts.length) { activePageId = drafts[0].id; } else { var newPage = createPageData(getNextDraftPageName(), []); newPage.projectPageNumber = getArchivedPages().length + 1; state.pages.push(newPage); activePageId = newPage.id; } renderActivePage(); recalculate(); } function unarchivePage() { var page = getActivePage(); if (!page || !page.archived) return; page.archived = false; renderActivePage(); recalculate(); } function removeReuseReferencesToPage(pageId) { state.pages.forEach(function (p) { p.rows = p.rows.filter(function (row) { return !(row.mode === 'reuse' && row.sourcePageId === pageId); }); }); } function switchAfterPageRemoved(removedIdx) { var drafts = getDraftPages(); if (drafts.length) { activePageId = drafts[0].id; return; } if (state.pages.length) { var next = state.pages[Math.min(removedIdx, state.pages.length - 1)]; activePageId = next.id; return; } var newPage = createPageData(getNextDraftPageName(), []); state.pages.push(newPage); activePageId = newPage.id; } function deleteArchivedPageById(pageId) { var page = state.pages.find(function (p) { return p.id === pageId; }); if (!page || !page.archived) return; var label = page.name || 'این صفحه'; if (!window.confirm('صفحه آرشیوشده «' + label + '» حذف شود؟\nبلاک‌های Reuse مرتبط با این صفحه هم حذف می‌شوند.')) { return; } removeReuseReferencesToPage(pageId); var idx = getPageIndex(pageId); state.pages.splice(idx, 1); if (activePageId === pageId) { switchAfterPageRemoved(idx); renderActivePage(); } recalculate(); } function deleteArchivedPage() { var page = getActivePage(); if (!page || !page.archived) return; deleteArchivedPageById(page.id); } function deletePage() { var page = getActivePage(); if (!page || page.archived) return; if (state.pages.length <= 1) return; saveActivePageFromDom(); var idx = getPageIndex(activePageId); state.pages.splice(idx, 1); var next = state.pages[Math.min(idx, state.pages.length - 1)]; activePageId = next.id; renderActivePage(); recalculate(); } var VALID_COMPLEXITY = { simple: true, medium: true, complex: true }; function blockToSlug(name) { return name.toLowerCase().replace(/\s+/g, '-'); } function normalizeToken(str) { return (str || '').trim().toLowerCase().replace(/[_-]+/g, ' ').replace(/\s+/g, ' '); } function getCatalogIndex() { var index = []; Object.keys(CATALOG).forEach(function (catKey) { Object.keys(CATALOG[catKey].blocks).forEach(function (blockName) { index.push({ cat: catKey, block: blockName, normalized: normalizeToken(blockName), slug: blockToSlug(blockName), compact: normalizeToken(blockName).replace(/\s/g, '') }); }); }); return index; } var catalogIndex = getCatalogIndex(); function resolveCatalogBlock(blockName, catHint) { if (!blockName) return null; var normalized = normalizeToken(blockName); var compact = normalized.replace(/\s/g, ''); var slug = blockName.trim().toLowerCase().replace(/\s+/g, '-'); if (catHint && CATALOG[catHint] && CATALOG[catHint].blocks[blockName]) { return { cat: catHint, block: blockName }; } for (var i = 0; i < catalogIndex.length; i++) { var item = catalogIndex[i]; if (item.block === blockName) return { cat: item.cat, block: item.block }; if (item.normalized === normalized || item.compact === compact || item.slug === slug) { return { cat: item.cat, block: item.block }; } } return null; } function normalizeComplexity(value) { var v = (value || '').trim().toLowerCase(); return VALID_COMPLEXITY[v] ? v : 'medium'; } function makeImportRow(cat, block, complexity, extra) { var row = { mode: 'new', cat: cat, block: block, complexity: normalizeComplexity(complexity), reuse: 'new_block' }; if (extra) { Object.keys(extra).forEach(function (k) { row[k] = extra[k]; }); } return row; } var BLOCK_CLASSIFIERS = [ { cat: 'general', block: 'Navbar', weight: 10, test: function (el, ctx) { if (el.matches('header, [role="banner"]')) return true; if (el.matches('nav') && !el.closest('footer')) return true; return /nav(bar|igation)?/i.test(ctx.hay); }}, { cat: 'general', block: 'Footer', weight: 10, test: function (el, ctx) { return el.matches('footer, [role="contentinfo"]') || /footer/i.test(ctx.hay); }}, { cat: 'general', block: 'Hero', weight: 9, test: function (el, ctx) { return /hero|jumbotron|masthead|page-header/i.test(ctx.hay) && !/feature/i.test(ctx.hay); }}, { cat: 'general', block: 'Pricing', weight: 9, test: function (el, ctx) { return /pricing|price-table|plans?/i.test(ctx.hay); }}, { cat: 'general', block: 'FAQ', weight: 9, test: function (el, ctx) { return /faq|frequently-asked|accordion.*question/i.test(ctx.hay) || (el.querySelector('details, [data-slot="accordion"]') && /faq|question/i.test(ctx.hay)); }}, { cat: 'general', block: 'Testimonial', weight: 8, test: function (el, ctx) { return /testimonial|review|quote|customer-story/i.test(ctx.hay); }}, { cat: 'general', block: 'Gallery', weight: 8, test: function (el, ctx) { return /gallery|portfolio|lightbox/i.test(ctx.hay) || (el.querySelectorAll('img').length >= 4 && /gallery|portfolio/i.test(ctx.hay)); }}, { cat: 'general', block: 'Feature Grid', weight: 8, test: function (el, ctx) { return /feature|benefit|capabilit/i.test(ctx.hay) && (el.querySelectorAll('[class*="card"], [class*="grid"] > *').length >= 3 || /grid/i.test(ctx.hay)); }}, { cat: 'general', block: 'Card Section', weight: 7, test: function (el, ctx) { return el.querySelectorAll('[class*="card"]').length >= 2 && !/pricing|testimonial/i.test(ctx.hay); }}, { cat: 'general', block: 'CTA', weight: 8, test: function (el, ctx) { return /(^|\s)cta(\s|$)|call-to-action|get-started/i.test(ctx.hay); }}, { cat: 'general', block: 'Banner', weight: 7, test: function (el, ctx) { return /banner|promo|announcement/i.test(ctx.hay) && !/hero/i.test(ctx.hay); }}, { cat: 'general', block: 'Timeline', weight: 7, test: function (el, ctx) { return /timeline|roadmap|history/i.test(ctx.hay); }}, { cat: 'general', block: 'Sidebar', weight: 8, test: function (el, ctx) { return el.matches('aside') || /sidebar|side-nav/i.test(ctx.hay); }}, { cat: 'general', block: 'Blog List', weight: 8, test: function (el, ctx) { return /blog|article-list|posts?/i.test(ctx.hay) && el.querySelectorAll('article, [class*="post"]').length >= 2; }}, { cat: 'general', block: 'Contact Section', weight: 8, test: function (el, ctx) { return /contact|get-in-touch/i.test(ctx.hay) || (el.querySelector('form') && /contact/i.test(ctx.hay)); }}, { cat: 'dashboard', block: 'Data Table', weight: 9, test: function (el, ctx) { return el.querySelector('table') && /table|data-grid|datatable/i.test(ctx.hay); }}, { cat: 'dashboard', block: 'Stat Card', weight: 8, test: function (el, ctx) { return /stat|metric|kpi|counter/i.test(ctx.hay); }}, { cat: 'dashboard', block: 'Chart', weight: 8, test: function (el, ctx) { return /chart|graph|analytics/i.test(ctx.hay) || el.querySelector('canvas, svg[class*="chart"]'); }}, { cat: 'dashboard', block: 'Filter Bar', weight: 7, test: function (el, ctx) { return /filter|facets?/i.test(ctx.hay); }}, { cat: 'dashboard', block: 'Widget', weight: 6, test: function (el, ctx) { return /widget|panel/i.test(ctx.hay); }}, { cat: 'form', block: 'Form Section', weight: 9, test: function (el, ctx) { return el.querySelector('form') && !/search|login|sign-?in/i.test(ctx.hay); }}, { cat: 'form', block: 'Multi Step', weight: 9, test: function (el, ctx) { return /multi-?step|wizard|stepper/i.test(ctx.hay); }}, { cat: 'form', block: 'File Upload', weight: 8, test: function (el, ctx) { return el.querySelector('input[type="file"]') || /upload|dropzone/i.test(ctx.hay); }}, { cat: 'interactive', block: 'Modal', weight: 9, test: function (el, ctx) { return el.matches('[role="dialog"], [data-slot="dialog"], .modal') || /modal|dialog/i.test(ctx.hay); }}, { cat: 'interactive', block: 'Drawer', weight: 8, test: function (el, ctx) { return /drawer|sheet|offcanvas/i.test(ctx.hay); }} ]; var STRUCTURAL_HEURISTICS = [ { selector: 'header, [role="banner"]', cat: 'general', block: 'Navbar' }, { selector: 'nav:not(header nav):not(footer nav)', cat: 'general', block: 'Navbar' }, { selector: 'footer, [role="contentinfo"]', cat: 'general', block: 'Footer' }, { selector: '[class*="hero" i], [id*="hero" i], [data-slot*="hero" i]', cat: 'general', block: 'Hero' }, { selector: '[class*="pricing" i], [id*="pricing" i]', cat: 'general', block: 'Pricing' }, { selector: '[class*="faq" i], [id*="faq" i]', cat: 'general', block: 'FAQ' }, { selector: '[class*="testimonial" i], [id*="testimonial" i]', cat: 'general', block: 'Testimonial' }, { selector: '[class*="gallery" i], [id*="gallery" i]', cat: 'general', block: 'Gallery' }, { selector: '[class*="sidebar" i], aside', cat: 'general', block: 'Sidebar' }, { selector: '[class*="cta" i], [id*="cta" i]', cat: 'general', block: 'CTA' }, { selector: '[class*="banner" i], [id*="banner" i]', cat: 'general', block: 'Banner' }, { selector: '[class*="timeline" i], [id*="timeline" i]', cat: 'general', block: 'Timeline' }, { selector: '[class*="feature-grid" i], [class*="features" i][class*="grid" i]', cat: 'general', block: 'Feature Grid' }, { selector: '[class*="contact" i][class*="section" i], [id*="contact" i]', cat: 'general', block: 'Contact Section' }, { selector: '[class*="data-table" i], table[data-slot], .data-table', cat: 'dashboard', block: 'Data Table' }, { selector: '[class*="stat-card" i], [class*="stats" i]', cat: 'dashboard', block: 'Stat Card' }, { selector: '[class*="chart" i]:not(canvas)', cat: 'dashboard', block: 'Chart' }, { selector: '[role="dialog"], [data-slot="dialog"], .modal', cat: 'interactive', block: 'Modal' }, { selector: '[data-slot="drawer"], .drawer', cat: 'interactive', block: 'Drawer' }, { selector: '[data-slot="dropdown-menu"], .dropdown-menu', cat: 'interactive', block: 'Dropdown' } ]; function getElementHaystack(el) { var parts = []; if (el.id) parts.push(el.id); if (el.className && typeof el.className === 'string') parts.push(el.className); if (el.getAttribute('data-slot')) parts.push(el.getAttribute('data-slot')); if (el.getAttribute('data-section')) parts.push(el.getAttribute('data-section')); if (el.getAttribute('aria-label')) parts.push(el.getAttribute('aria-label')); var heading = el.querySelector('h1, h2, h3'); if (heading) parts.push(heading.textContent || ''); return parts.join(' ').toLowerCase(); } function classifyElement(el) { var ctx = { hay: getElementHaystack(el) }; var best = null; var bestWeight = 0; BLOCK_CLASSIFIERS.forEach(function (rule) { try { if (rule.test(el, ctx) && rule.weight > bestWeight) { best = rule; bestWeight = rule.weight; } } catch (e) { /* ignore */ } }); if (best) { return { cat: best.cat, block: best.block, complexity: inferComplexity(el, best.block) }; } return null; } function inferComplexity(el, blockName) { var childCount = el.querySelectorAll('*').length; var inputs = el.querySelectorAll('input, select, textarea, button').length; var cards = el.querySelectorAll('[class*="card"], li, article').length; if (blockName === 'Data Table' || blockName === 'Multi Step' || blockName === 'Kanban') { if (childCount > 80 || inputs > 8) return 'complex'; if (childCount > 35 || inputs > 3) return 'medium'; return 'simple'; } if (blockName === 'Hero' || blockName === 'Pricing' || blockName === 'Gallery') { if (childCount > 60 || cards > 6) return 'complex'; if (childCount > 25 || cards > 3) return 'medium'; return 'simple'; } if (childCount > 50 || inputs > 5 || cards > 5) return 'complex'; if (childCount > 20 || inputs > 2 || cards > 2) return 'medium'; return 'simple'; } function findMainRoot(doc) { return doc.querySelector('main, [role="main"], #main, .main, #content, .content, .page, [class*="page-content"]') || doc.body; } function isHiddenElement(el) { if (!el || el.nodeType !== 1) return true; if (el.closest('[hidden], [aria-hidden="true"], template, script, style, noscript')) return true; var tag = el.tagName.toLowerCase(); if (tag === 'script' || tag === 'style' || tag === 'noscript' || tag === 'link' || tag === 'meta') return true; if (el.matches('[data-elementor-type="popup"], [data-elementor-type="floating-buttons"], .elementor-location-popup, .elementor-location-floating_buttons')) return true; return false; } function getSectionText(el) { return (el.textContent || '').replace(/\s+/g, ' ').trim(); } function hasMeaningfulContent(el) { var text = getSectionText(el); if (text.length > 12) return true; return !!el.querySelector('img, svg, video, form, table, input, button, a[href], canvas'); } function getHiddenBreakpoints(el) { var cls = (el.className && typeof el.className === 'string') ? el.className : ''; var matches = cls.match(/elementor-hidden-([a-z_]+)/gi) || []; return matches.map(function (m) { return m.toLowerCase(); }); } function areResponsiveAlternates(a, b) { var aHidden = getHiddenBreakpoints(a); var bHidden = getHiddenBreakpoints(b); if (!aHidden.length || !bHidden.length) return false; if (aHidden.join('|') === bHidden.join('|')) return false; var aWidgets = getWidgetFingerprint(a); var bWidgets = getWidgetFingerprint(b); if (aWidgets && aWidgets === bWidgets) return true; var aHeading = (a.querySelector('h1, h2, h3') || {}).textContent || ''; var bHeading = (b.querySelector('h1, h2, h3') || {}).textContent || ''; if (aHeading && bHeading && aHeading.trim() === bHeading.trim()) return true; return false; } function getWidgetFingerprint(el) { var types = []; el.querySelectorAll('[data-widget_type]').forEach(function (w) { var t = w.getAttribute('data-widget_type'); if (!t) return; if (t === 'image.default' || t === 'spacer.default') return; types.push(t); }); types.sort(); return types.join('|'); } function isHeadingOnlySection(el) { if (!el || el.matches('header, footer, nav')) return false; var widgets = el.querySelectorAll('[data-widget_type]'); if (widgets.length > 1) return false; var text = getSectionText(el); if (text.length > 50) return false; return !!el.querySelector('h1, h2, h3, h4'); } function filterHeadingOnlySections(sections) { return sections.filter(function (section) { if (section.role === 'layout-header' || section.role === 'layout-footer' || section.role === 'explicit') { return true; } return !isHeadingOnlySection(section.el); }).map(function (section, idx) { return Object.assign({}, section, { index: idx + 1 }); }); } function finalizeSections(sections) { return filterHeadingOnlySections(mergeResponsiveSections(sections)); } function mergeResponsiveSections(sections) { if (!sections.length) return []; var merged = []; var skip = Object.create(null); sections.forEach(function (section, i) { if (skip[i]) return; var group = [section]; for (var j = i + 1; j < sections.length; j++) { if (skip[j]) continue; if (areResponsiveAlternates(section.el, sections[j].el)) { group.push(sections[j]); skip[j] = true; } } merged.push({ el: section.el, source: section.source, role: section.role, index: merged.length + 1, mergedCount: group.length }); }); return merged; } function discoverExplicitSections(doc) { var sections = []; function add(el, source) { if (!el || isHiddenElement(el)) return; sections.push({ el: el, source: source, role: 'explicit' }); } doc.querySelectorAll('[data-feep-block]').forEach(function (el) { add(el, 'data-feep-block'); }); doc.querySelectorAll('[data-block]').forEach(function (el) { add(el, 'data-block'); }); doc.querySelectorAll('[data-block-name]').forEach(function (el) { add(el, 'data-block-name'); }); return sections; } function discoverElementorSections(doc) { var sections = []; var pageRoot = doc.querySelector('[data-elementor-type="wp-page"].elementor, .elementor-page.elementor'); if (pageRoot) { Array.from(pageRoot.children).forEach(function (child) { if (!child.matches('.e-con.e-parent')) return; if (isHiddenElement(child) || !hasMeaningfulContent(child)) return; sections.push({ el: child, source: 'elementor-section', role: 'content' }); }); } var header = doc.querySelector('.elementor-location-header, header.elementor[data-elementor-type="header"]'); if (header && hasMeaningfulContent(header)) { sections.unshift({ el: header, source: 'elementor-header', role: 'layout-header' }); } var footer = doc.querySelector('.elementor-location-footer, footer.elementor[data-elementor-type="footer"]'); if (footer && hasMeaningfulContent(footer)) { sections.push({ el: footer, source: 'elementor-footer', role: 'layout-footer' }); } return sections; } function discoverSemanticSections(doc) { var sections = []; var bodyHeader = doc.querySelector('body > header, body > [role="banner"]'); var bodyFooter = doc.querySelector('body > footer, body > [role="contentinfo"]'); var bodyNav = doc.querySelector('body > nav'); var root = findMainRoot(doc); if (bodyHeader && hasMeaningfulContent(bodyHeader)) { sections.push({ el: bodyHeader, source: 'semantic-header', role: 'layout-header' }); } else if (bodyNav && hasMeaningfulContent(bodyNav)) { sections.push({ el: bodyNav, source: 'semantic-nav', role: 'layout-header' }); } var mainChildren = []; if (root) { Array.from(root.children).forEach(function (child) { if (!child.matches('section, article, div, header, footer, aside, nav')) return; if (isHiddenElement(child) || !hasMeaningfulContent(child)) return; mainChildren.push(child); }); } if (!mainChildren.length) { doc.querySelectorAll('main section, main article, body > section, body > article').forEach(function (el) { if (isHiddenElement(el) || !hasMeaningfulContent(el)) return; mainChildren.push(el); }); } var unique = mainChildren.filter(function (el, idx, arr) { for (var i = 0; i < arr.length; i++) { if (arr[i] !== el && arr[i].contains(el)) return false; } return true; }); unique.forEach(function (el) { sections.push({ el: el, source: 'semantic-section', role: 'content' }); }); if (bodyFooter && hasMeaningfulContent(bodyFooter)) { sections.push({ el: bodyFooter, source: 'semantic-footer', role: 'layout-footer' }); } return sections; } function discoverPageSections(doc) { var explicit = discoverExplicitSections(doc); if (explicit.length) return finalizeSections(explicit); var elementor = discoverElementorSections(doc); if (elementor.length >= 2) return finalizeSections(elementor); var semantic = discoverSemanticSections(doc); if (semantic.length) return finalizeSections(semantic); return []; } function classifyByRole(section) { if (section.role === 'layout-header') { return { cat: 'general', block: 'Navbar', complexity: inferComplexity(section.el, 'Navbar') }; } if (section.role === 'layout-footer') { return { cat: 'general', block: 'Footer', complexity: inferComplexity(section.el, 'Footer') }; } return null; } function classifyByWidgets(el) { var widgetTypes = []; el.querySelectorAll('[data-widget_type]').forEach(function (w) { widgetTypes.push(w.getAttribute('data-widget_type') || ''); }); var joined = widgetTypes.join(' ').toLowerCase(); if (/nadernavigation|nav-menu|mega-menu/i.test(joined) || el.querySelector('nav, .nader-navigation, #nav')) { return { cat: 'general', block: 'Navbar', complexity: inferComplexity(el, 'Navbar') }; } if (/tabswithimage|nested-tabs|tabs\./i.test(joined)) { return { cat: 'dashboard', block: 'Tabs', complexity: inferComplexity(el, 'Tabs') }; } if (/postsslider|posts-slider|blog|loop-grid|archive-posts/i.test(joined)) { return { cat: 'general', block: 'Blog List', complexity: inferComplexity(el, 'Blog List') }; } if (/infinitecss|logo-slider|carousel|swiper|image-carousel/i.test(joined)) { return { cat: 'general', block: 'Testimonial', complexity: inferComplexity(el, 'Testimonial') }; } if (/accordion|faq/i.test(joined)) { return { cat: 'general', block: 'FAQ', complexity: inferComplexity(el, 'FAQ') }; } if (/form\./i.test(joined) || (el.querySelector('form') && !el.querySelector('header, nav'))) { return { cat: 'form', block: 'Form Section', complexity: inferComplexity(el, 'Form Section') }; } if (/price-table|pricing/i.test(joined)) { return { cat: 'general', block: 'Pricing', complexity: inferComplexity(el, 'Pricing') }; } if (/animated-headline|slides|video/i.test(joined) && el.querySelector('h1')) { return { cat: 'general', block: 'Hero', complexity: inferComplexity(el, 'Hero') }; } if (/animated-headline/i.test(joined)) { return { cat: 'general', block: 'Banner', complexity: inferComplexity(el, 'Banner') }; } return null; } function classifySection(section) { var roleMatch = classifyByRole(section); if (roleMatch) return Object.assign({ label: roleMatch.block }, roleMatch); var el = section.el; var widgetMatch = classifyByWidgets(el); if (widgetMatch) return Object.assign({ label: widgetMatch.block }, widgetMatch); var classified = classifyElement(el); if (classified) return Object.assign({ label: classified.block }, classified); var heading = el.querySelector('h1, h2, h3'); var headingText = heading ? heading.textContent.trim() : ''; return { cat: 'general', block: 'Card Section', complexity: inferComplexity(el, 'Card Section'), label: headingText ? 'Card Section — ' + headingText.slice(0, 30) : 'Card Section' }; } function parseExplicitMarkers(doc) { var rows = []; doc.querySelectorAll('script[type="application/json"][data-feep-blocks], script#feep-blocks').forEach(function (el) { try { var data = JSON.parse(el.textContent || '[]'); var list = Array.isArray(data) ? data : (data.blocks || []); list.forEach(function (item) { if (!item || !item.block) return; var match = resolveCatalogBlock(item.block, item.cat || item.category); if (match) rows.push(makeImportRow(match.cat, match.block, item.complexity)); }); } catch (e) { /* invalid json */ } }); var commentPattern = /feep:block(?::([^\s>]+))?(?::([^\s>]+))?(?::([^\s>]+))?/gi; var walker = doc.createTreeWalker(doc, NodeFilter.SHOW_COMMENT, null); var commentNode; while ((commentNode = walker.nextNode())) { var text = commentNode.nodeValue || ''; var match; commentPattern.lastIndex = 0; while ((match = commentPattern.exec(text))) { var c1 = match[1]; var c2 = match[2]; var c3 = match[3]; var catHint = null; var blockName = c1; var complexity = 'medium'; if (c2 && CATALOG[c1] && CATALOG[c1].blocks[c2]) { catHint = c1; blockName = c2; complexity = c3 || 'medium'; } else if (c2) { complexity = c2; } var resolved = resolveCatalogBlock(blockName, catHint); if (resolved) rows.push(makeImportRow(resolved.cat, resolved.block, complexity)); } } return rows; } function analyzePageFromHtml(html) { var doc = new DOMParser().parseFromString(html, 'text/html'); var markerRows = parseExplicitMarkers(doc); if (markerRows.length) { return { rows: markerRows, pageTitle: getPageTitle(doc), meta: { phase1Count: markerRows.length, phase2Count: markerRows.length, mergedResponsive: 0, source: 'markers' } }; } var rawSections = discoverExplicitSections(doc); if (!rawSections.length) { rawSections = discoverElementorSections(doc); } if (rawSections.length < 2) { rawSections = discoverSemanticSections(doc); } var phase1Count = rawSections.length; var sections = finalizeSections(rawSections); if (!sections.length) { return { rows: [], pageTitle: getPageTitle(doc), meta: { phase1Count: 0, phase2Count: 0, mergedResponsive: 0, source: 'none' } }; } var analyzed = sections.map(function (section) { var explicitBlock = section.role === 'explicit' ? section.el.getAttribute('data-feep-block') || section.el.getAttribute('data-block') || section.el.getAttribute('data-block-name') : null; var explicitCat = section.el.getAttribute('data-feep-category') || section.el.getAttribute('data-feep-cat') || section.el.getAttribute('data-block-category'); var explicitComplexity = section.el.getAttribute('data-feep-complexity') || section.el.getAttribute('data-block-complexity'); if (explicitBlock) { var resolved = resolveCatalogBlock(explicitBlock, explicitCat); if (resolved) { return { index: section.index, cat: resolved.cat, block: resolved.block, complexity: normalizeComplexity(explicitComplexity), label: resolved.block, fingerprint: section.el ? getWidgetFingerprint(section.el) : '', source: section.source }; } } var classified = classifySection(section); return { index: section.index, cat: classified.cat, block: classified.block, complexity: classified.complexity, label: classified.label, fingerprint: section.el ? getWidgetFingerprint(section.el) : '', source: section.source }; }); var rows = analyzed.map(function (item) { return makeImportRow(item.cat, item.block, item.complexity, { label: item.label || item.block, fingerprint: item.fingerprint || '' }); }); var mergedResponsive = Math.max(0, phase1Count - sections.length); return { rows: rows, analyzed: analyzed, pageTitle: getPageTitle(doc), meta: { phase1Count: phase1Count, phase2Count: analyzed.length, mergedResponsive: mergedResponsive, source: sections[0] ? sections[0].source : 'none' } }; } function getPageTitle(doc) { var pageName = doc.querySelector('[data-feep-page-name]'); if (pageName) return pageName.getAttribute('data-feep-page-name'); return (doc.querySelector('title') && doc.querySelector('title').textContent || '').trim(); } function parseBlocksFromHtml(html) { var result = analyzePageFromHtml(html); return { rows: result.rows, pageTitle: result.pageTitle, meta: result.meta, analyzed: result.analyzed }; } function fetchPageHtml(url) { var localApi = '/api/fetch-page?url=' + encodeURIComponent(url); return fetch(localApi, { method: 'GET', credentials: 'same-origin' }) .then(function (res) { if (res.ok) return res.text(); throw new Error('local-api'); }) .catch(function () { return fetch(url, { method: 'GET', credentials: 'omit' }) .then(function (res) { if (!res.ok) throw new Error('HTTP ' + res.status); return res.text(); }); }) .catch(function () { var proxyUrl = 'https://api.allorigins.win/raw?url=' + encodeURIComponent(url); return fetch(proxyUrl, { method: 'GET' }).then(function (res) { if (!res.ok) throw new Error('proxy'); return res.text(); }); }); } function setImportStatus(type, message) { var el = document.getElementById('calc-import-status'); if (!el) return; el.hidden = false; el.className = 'calc-import-status is-' + type; el.textContent = message; } function clearImportStatus() { var el = document.getElementById('calc-import-status'); if (!el) return; el.hidden = true; el.className = 'calc-import-status'; el.textContent = ''; } function applyImportedRows(rows, pageTitle, options) { var page = getActivePage(); if (!page || page.archived) { setImportStatus('error', 'صفحه فعال قابل ویرایش نیست — یک پیش‌نویس انتخاب کنید.'); return false; } if (!rows.length) { setImportStatus('error', 'هیچ بلاکی در این صفحه شناسایی نشد. از data-feep-block روی المان‌ها استفاده کنید یا ساختار HTML را واضح‌تر کنید.'); return false; } saveActivePageFromDom(); if (options.replace) { page.rows = rows.map(function (r) { return Object.assign({ id: nextRowId() }, r); }); } else { rows.forEach(function (r) { page.rows.push(Object.assign({ id: nextRowId() }, r)); }); } resolveIntraPageReuseReferences(page); dedupeRowsWithinPage(page); if (options.useTitle && pageTitle) { page.name = pageTitle.split(/[|\-–—]/)[0].trim() || pageTitle; } if (options.sourceUrl) { page.sourceUrl = options.sourceUrl; } if (options.projectPageNumber) { page.projectPageNumber = options.projectPageNumber; } sanitizePageLayoutRows(page); renderActivePage(); recalculate(); return true; } function autoArchiveActivePage() { var page = getActivePage(); if (!page || page.archived || !page.rows.length) return false; saveActivePageFromDom(); if (!page.projectPageNumber) { page.projectPageNumber = getArchivedPages().length + 1; } sanitizePageLayoutRows(page); page.archived = true; if (!page.archivedAt) { page.archivedAt = new Date().toISOString(); } var newPage = createPageData(getNextDraftPageName(), []); newPage.projectPageNumber = getArchivedPages().length + 1; state.pages.push(newPage); activePageId = newPage.id; renderActivePage(); recalculate(); return true; } function buildImportStatusMessage(pageNum, matched, meta, archived) { var parts = []; if (meta) { parts.push('فاز ۱: ' + meta.phase1Count + ' بلاک شمارش شد'); if (meta.mergedResponsive) { parts.push(meta.mergedResponsive + ' نسخه ریسپانسیو ادغام شد'); } parts.push('فاز ۲: ' + meta.phase2Count + ' بلاک شناسایی شد'); } parts.push('صفحه ' + pageNum + ' («' + matched.page.name + '»)'); if (matched.stats.reuseCount) parts.push(matched.stats.reuseCount + ' Reuse'); if (matched.stats.newCount) parts.push(matched.stats.newCount + ' جدید'); if (matched.stats.skippedLayout) parts.push(matched.stats.skippedLayout + ' هدر/فوتر حذف شد'); parts.push(archived ? 'فاز ۳: ثبت و آرشیو شد' : 'فاز ۳: به پیش‌نویس اضافه شد'); return parts.join(' — '); } function importFromUrl() { var urlInput = document.getElementById('calc-import-url'); var btn = document.getElementById('calc-import-btn'); if (!urlInput || !btn) return; var rawUrl = urlInput.value.trim(); if (!rawUrl) { setImportStatus('error', 'لطفاً آدرس URL را وارد کنید.'); return; } var url; try { url = new URL(rawUrl).href; } catch (e) { setImportStatus('error', 'آدرس URL معتبر نیست.'); return; } var useTitle = document.getElementById('calc-import-title'); var autoArchive = document.getElementById('calc-import-auto-archive'); var existingPageWithUrl = findPageBySourceUrl(url); var pageNum = getSequentialPageNumber(); var excludeLayout = pageNum > 1; var page = getActivePage(); if (!page || page.archived) { var drafts = getDraftPages(); if (drafts.length) { activePageId = drafts[0].id; page = getActivePage(); } else { var freshPage = createPageData(getNextDraftPageName(), []); state.pages.push(freshPage); activePageId = freshPage.id; page = freshPage; } } btn.disabled = true; setImportStatus('loading', 'در حال دریافت صفحه ' + pageNum + ' و تحلیل بلاک‌ها…' + (excludeLayout ? ' (بدون هدر/فوتر)' : '')); fetchPageHtml(url) .then(function (html) { var parsed = resolveUrlAnalysis(url, html); var matched; if (pageNum === 1 && existingPageWithUrl) { matched = matchRowsAsNew(parsed.rows, { excludeLayout: false }); } else { matched = matchRowsWithReuse(parsed.rows, { excludeLayout: excludeLayout }); } var applied = applyImportedRows(matched.rows, parsed.pageTitle, { replace: true, useTitle: useTitle ? useTitle.checked : true, sourceUrl: url, projectPageNumber: pageNum }); if (!applied) return; var importedPage = getActivePage(); var shouldArchive = !autoArchive || autoArchive.checked; if (shouldArchive) { autoArchiveActivePage(); } setImportStatus('success', buildImportStatusMessage(pageNum, { page: importedPage, rows: matched.rows, stats: matched.stats }, parsed.meta, shouldArchive)); urlInput.value = ''; urlInput.focus(); }) .catch(function () { setImportStatus('error', 'دریافت صفحه ممکن نشد. ابتدا با npm run dev سرور را اجرا کنید، سپس URL را دوباره امتحان کنید.'); }) .finally(function () { btn.disabled = false; }); } function initCalculator() { rowsEl = document.getElementById('calc-rows'); emptyEl = document.getElementById('calc-empty'); tabsEl = document.getElementById('calc-page-tabs'); pagesListEl = document.getElementById('calc-pages-list'); archiveListEl = document.getElementById('calc-archive-list'); calcMainEl = document.getElementById('calc-main'); if (!rowsEl || !document.getElementById('calc-add')) { return; } var homePage = createPageData('صفحه ۱', DEFAULT_ROWS); homePage.projectPageNumber = 1; state.pages.push(homePage); activePageId = homePage.id; document.getElementById('calc-add').addEventListener('click', function () { createRow(); }); document.getElementById('calc-reuse').addEventListener('click', addReuseRow); document.getElementById('calc-page-add').addEventListener('click', addPage); document.getElementById('calc-page-delete').addEventListener('click', deletePage); document.getElementById('calc-page-archive').addEventListener('click', archivePage); document.getElementById('calc-page-unarchive').addEventListener('click', unarchivePage); var deleteArchiveBtn = document.getElementById('calc-page-delete-archive'); if (deleteArchiveBtn) { deleteArchiveBtn.addEventListener('click', deleteArchivedPage); } document.getElementById('calc-page-name').addEventListener('input', recalculate); loadProjectTitleFromStorage(); var projectTitleEl = document.getElementById('opt-project-title'); if (projectTitleEl) { projectTitleEl.addEventListener('input', function () { saveProjectTitleToStorage(); recalculate(); }); } ['opt-content', 'opt-responsive', 'opt-dark', 'opt-bilingual', 'opt-animation'].forEach(function (id) { document.getElementById(id).addEventListener('change', function () { var wrap = document.getElementById('anim-extra-wrap'); if (wrap) { wrap.classList.toggle('hidden', document.getElementById('opt-animation').value !== 'complex'); } recalculate(); }); }); document.getElementById('opt-anim-units').addEventListener('input', recalculate); var pageMultEl = document.getElementById('opt-page-multiplier'); if (pageMultEl) pageMultEl.addEventListener('input', recalculate); var revisionEl = document.getElementById('opt-revision-units'); if (revisionEl) revisionEl.addEventListener('input', recalculate); var importBtn = document.getElementById('calc-import-btn'); var importUrl = document.getElementById('calc-import-url'); if (importBtn) { importBtn.addEventListener('click', importFromUrl); } if (importUrl) { importUrl.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); importFromUrl(); } }); importUrl.addEventListener('input', clearImportStatus); } var reportCopyBtn = document.getElementById('calc-report-copy'); var reportDownloadBtn = document.getElementById('calc-report-download'); var reportDownloadTxtBtn = document.getElementById('calc-report-download-txt'); var reportDownloadPdfBtn = document.getElementById('calc-report-download-pdf'); var reportSelectBtn = document.getElementById('calc-report-select'); var reportRefreshBtn = document.getElementById('calc-report-refresh'); var reportBody = getReportBodyEl(); if (reportCopyBtn) reportCopyBtn.addEventListener('click', copyProjectReport); if (reportDownloadBtn) reportDownloadBtn.addEventListener('click', downloadProjectReport); if (reportDownloadTxtBtn) reportDownloadTxtBtn.addEventListener('click', downloadProjectReportText); if (reportDownloadPdfBtn) reportDownloadPdfBtn.addEventListener('click', downloadProjectReportPdf); if (reportSelectBtn) reportSelectBtn.addEventListener('click', selectProjectReport); if (reportRefreshBtn) reportRefreshBtn.addEventListener('click', refreshProjectReport); if (reportBody) { reportBody.addEventListener('input', onReportEdited); try { var savedHtml = localStorage.getItem('feep-price-report-html'); if (savedHtml) { reportBody.innerHTML = savedHtml; state.reportEdited = true; } } catch (e) { /* ignore */ } } renderActivePage(); recalculate(); } function initCollapse() { document.querySelectorAll('[data-collapse]').forEach(function (el) { var trigger = el.classList.contains('table-collapse') ? el.querySelector('.table-collapse-trigger') : el.querySelector('.section-trigger'); if (!trigger) return; trigger.addEventListener('click', function () { var open = el.classList.toggle('is-open'); trigger.setAttribute('aria-expanded', open ? 'true' : 'false'); }); }); } function init() { initCalculator(); initCollapse(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();